I have a two level deep nesting:
routes.rb
resources :projects do
resources :offers do
resources :reports
end
end
In offers controller I use:
before_action :set_project
before_action :set_offer, only: [:show, :edit, :update, :destroy]
.....
def index
#offers = #project.offers
end
.....
def set_project
#project = Project.find(params[:project_id])
end
# Use callbacks to share common setup or constraints between actions.
def set_offer
#offer = #project.offers.find(params[:id])
end
How can I reference repors in Reports_controller.rb like in offer's index?
It's the same idea:
# reports_controller.rb
def index
#reports = Reports.where(project: params[:project_id],
offer: params[:offer_id])
end
def update
#report = Reports.find_by(project: params[:project_id],
offer: params[:offer_id],
id: params[:id])
# rest of update code
end
You could put this logic into helper methods if you want like you did for the other ones.
I would also just mention that it's usually discouraged to nest much more than one level deep like this because, as you can see, it's getting kind of messy. One compromise that I use a lot is to use the shallow resources method.
Definitely check out this section of the Rails Routing guide for more info on this and how to implement shallow nesting. It basically lets you make it so that you only require the additional parameters for actions that actually need that information, but the other actions let you reference the resource by just its id without the need for the other parameters.
Related
I have some nested routes set up like below:-
resources :events, only: :index
resources :organisations do
resources :events, except: [:index]
end
The allows me to have the following routes:-
/events
/organisations/1/events/1
Both of the above work fine, however, I can change the event id in the URL, and it will give me an event that doesn't belong to the organisation, yet the organisation id will remain the same:-
/organisations/1/events/2
i.e. this will show me Event #2, but the organisation for this event is now Organisation #1.
In my EventsController, I just have the usual generated plain route:-
class EventsController < ApplicationController
before_action :set_event, only: %i[show edit update destroy]
before_action :set_organisation, except: %i[index]
# GET /events/1
def show; end
# Use callbacks to share common setup or constraints between actions.
def set_event
#event = Event.find(params[:id])
end
def set_organisation
#organisation = Organisation.find(params[:organisation_id])
end
My question:- Should Rails know how to handle this and have I set something up incorrectly here, or do I just need to manually handle it by doing a check in my show method, that the current event belongs to the current organisation (and redirect somewhere if not)?
You need to manually handle it.
In set_event Event.find(params[:id]) finds record directly without any organsaction checks.
One way to handle it is by using scopes. If you pass an event id that does not belong to organisation you will receive RecordNotFound.
def set_event
#event = #organisation.events.find(params[:id])
end
Also you need to change order of callbacks: set_organisation should be called before set_event.
Is there an easy way to allow users who created their own project able to edit their work?
class Project < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :projects
end
How do I check if the current_user that is logged in can actually edit their stuff?
If my projects URL's are something like localhost:3000/projects/24, I want only the user who created this project can go into localhost:3000/projects/24/edit to view the page and actually edit...
At this time of writing, now I'm thinking this might not be he best way? Maybe I need to somehow do localhost:3000/projects/username1/24 or something? And then if they edit, it'll be localhost:3000/projects/username1/24/edit.... How can I accomplish this?
My routes:
Rails.application.routes.draw do
devise_for :users
get 'users/:id' => 'users#show', as: :user
resources :projects
end
My controller is just basic stuff from scaffolding
In your projects controller, add devise's authentication:
before_action :authenticate_user!, only: [:edit, :update]
This will ensure that the user is logged in when trying to view the edit page.
Next, you want to ensure that the logged-in user is the owner of the project.
To do this, you'll need to modify the edit and update methods to find the project by more than just the params:
def edit
#project = current_user.projects.find(params[:id])
#...
end
def update
#project = current_user.projects.find(params[:id])
#...
end
Now, only the current_user who owns the project can view the edit page and send the updates.
Bonus points: If you want to refactor the code above and not use the same "#project = " line twice, you can create another before_action which will assign the #project for both edit and update:
before_action :authenticate_user!, only: [:edit, :update]
before_action :find_project_by_user, only: [:edit, :update]
private
def find_project_by_user
#project = current_user.projects.find(params[:id])
end
Doing this, you won't have to add the same "#project = " line into both the edit and update methods.
Maybe I need to somehow do localhost:3000/projects/username1/24 or
something? And then if they edit, it'll be
localhost:3000/projects/username1/24/edit.... How can I accomplish
this?
Since user has many projects, you would probably want the url of the type:
localhost:3000/users/1/projects/2/edit
To accomplish this you would need the following setup:
#routes.rb
resources :users, shallow: true do # notice shallow, it will eliminate users/ from the path, but as I'm not mistaken /user_id/ as well..
resources :projects
end
The controller projects should be put under:
app/controllers/users/projects_controller.rb
and it should look like
class Users
class Projects
#...
end
end
With this setup you'll ensure, that user only see his projects.
def index
#projects = Projects.all # user can see all projects
#...
end
def show
#project = Projects.find(params[:project_id])
#...
end
def edit
#project = current_user.projects.find(params[:project_id]) # can edit only those, which he associated with
# ...
end
And just make sure you are making the link_to edit visible only for the user who can edit.
As to paths:
there are two options:
resources :users, path: '' do # will hide the users part from path
#...
end
resources :users, shallow: true do # will actually also hide the users/user_id part
#...
end
What you really want is to move beyond authentication, into authorization. Authentication validates that the user is who they say they are. Authorization validates that a user is allowed to perform a specific action on a specific object. Check out the Pundit Gem.. Its really simple and easy to use.
I wonder what is a proper way to resolve the following problem.
I have two models :
Publisher and Issue. Publisher has_many issues.
I want to manage issues both from Publishers list and from Issues list.
For example :
on publishers list user can click link "Issues" placed near each publisher. then he goes to Issues list but filtered only for proper publisher. He can click "create new issue" and goes to form for add new issue. On this form i don't need to show him a select list to choose publisher
on Issues list user can click "create new issue" and go to form but this time he should choose publisher from select, which will be related to created issue.
Simply speaking i need crud action for issue alone and for publisher issue.
First I try to make a :
resources :issues
resources :publishers do
resources :issues
end
and in issue controller :
before_filter :find_issue
def find_issue
#publisher = Publisher.find(params[:publisher_id]) if params[:publisher_id]
#issues = #publisher ? #publisher.issues : Issue
end
but i have to make many if condition in my views and controller.
For example if issue is created from publisher , on success i want to redirect to publisher_issues_path instead of issue_path a vice versa. Same problem with all link like "back to list" and similar. So code is in my opinion not very transparent.
Now i wonder to use namespaces.
namespace :publishers, do
resources :issues
end
and make
# app/controllers/publishers/issues_controller.rb
module Publishers
class IssuesController < ApplicationController
# actions that expect a :publisher_id param
end
end
# app/controllers/issues_controller.rb
class IssuesController < ApplicationController
# your typical actions without any publisher handling
end
and make separate view for both controller actions.
Is there a better or cleaner way to resolve this kind of problem? I want to make my code dry as possible.
Many thanks for reply.
Routes:
resources :issues
resources :publishes do
resources :issues
end
Controller:
class IssuesController < ApplicationController
before_filter :build_issue, only: [:new, :create]
before_filter :load_issue, only: [:show, :edit, :update, :destroy]
def index
#issues = issues.page(params[:page])
end
... all other actions will have access to #issue
private
def issues
if params[:publisher_id].present?
Publisher.find(params[:publisher_id]).issues
else
Issue
end
rescue ActiveRecord::RecordNotFound
redirect_to issues_path(alert: 'Publisher not found')
end
def build_issue
#issue = issues.new(issue_params)
end
def load_issue
#issue = issues.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to issues_path(alert: 'Issue not found')
end
def issue_params
# whitelisted attributes goes here
end
end
To avoid using conditions, use actions instead of full named path, i.e:
redirect_to action: :index
link_to 'Issues', {action: :index}
Playing around with Rails 4, I noticed that it defines a before_action filter set_[model], that is called for the actions show, edit, update and destroy.
It is in fact very useful, but I don't see much sense in not having it to all member actions. After all, if you are using a member action, you want to act on that member, thus you need to recover it from database at some point.
Note that by member actions, I also means the ones configured on routes.rb in the members block.
Is there a straight-forward way to do this, without list all member actions on the before_action filter?
Edit: To clarify, the whole point is use some rails magic to get all member routes and generate the array that would be pass in the :only. So I can do something like
before_action set_model only: all_member_routes
where all_member_routes is a piece of code that returns all member routes for my model.
Or even better,
before_action set_model, only_member_actions: true
The show, edit, update, destroy actions are precisely all member actions. They are the only ones that require to find the model.
If you have your own member action, then you'll have to add it to the list yourself.
before_action :set_item, only: [:edit, ..., :my_member_action]
Or, you can use the :except option to exclude all the collection actions that do not need it:
before_action :set_item, except: [:index, :create]
This way, if you add other member actions, you wont have to change anything.
Personally, I prefer to be explicit and use :only.
I'm pretty sure there is no easier way to do it, you can't detect all member actions automatically.
edit:
I really don't think you should do that but...
You can access the name of your controller with the controller_name method.
Getting the routes related to the controller:
routes = Rails.application.routes.routes.select { |r| r.defaults[:controller] == controller_name }
Then, I think the best way to see if a route is a member route is that the #parts array includes :id. Maybe you can find a more robust way.
So I would do:
routes.select { |r| r.parts.include?(:id) }.map { |r| r.defaults[:action] }.map &:to_sym
That would give you: [:show, :preview, :my_challenges] for
resources :users, only: [:index, :show], controller: 'accounts/users' do
member do
get :preview
get :my_challenges
end
end
class ApplicationController < ActionController::Base
def member_routes
Rails.application.routes.routes
.select { |r| r.defaults[:controller] == controller_name && r.parts.include?(:id) }
.map { |r| r.defaults[:action] }
.map(&:to_sym)
end
end
class UsersController < ApplicationController
before_action set_model, only: member_routes
end
If you ask for a before_action without :only or :except options, it will apply to all member actions:
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url # halts request cycle
end
end
end
In this particular case, it will require login from all actions on all controllers, since controllers will inherit from ApplicationController.
You can skip a before_action if you need it (for example, you need to skip require_login if you want to login into the system or sign up) like this:
class LoginsController < ApplicationController
skip_before_action :require_login, only: [:new, :create]
end
Source: Rails Guides
So, in your particular case:
You could have one usual UserController:
class UserController < ApplicationController
def index
end
...
/* you define here `index`, `create`, `update`, ... */
def destroy
...
end
end
And you could have a separate controller with all your member actions:
class UserCustomController < ApplicationController
before_action :set_model
def profile
...
end
def preview
end
def custom_member_action
end
...
/* all your member actions */
end
This would be actually better than having a single controller with lots of methods.
Basically I want to implement a simple Rails extension to define the seriousness of methods in my controller so that I can restrict usage of them appropriately. For example I'd define the default restful actions as so in an abstract superclass:
view_methods :index, :show
edit_methods :new, :create, :edit, :update
destroy_methods :destroy
I'd then down in a non-abstract controller call:
edit_methods :sort
to add in the sort method on that particular controller as being an edit level method.
I could then use a before_filter to check the level of the action currently being performed, and abort it if my logic determines that the current user can't do it.
Trouble is, I'm having trouble working out how to set up this kind of structure. I've tried something like this so far:
class ApplicationController
##view_methods = Array.new
##edit_methods = Array.new
##destroy_methods = Array.new
def self.view_methods(*view_methods)
class_variable_set(:##view_methods, class_variable_get(:##view_methods) << view_methods.to_a)
end
def self.edit_methods(*edit_methods)
class_variable_set(:##edit_methods, self.class_variable_get(:##edit_methods) << edit_methods.to_a)
end
def self.destroy_methods(*destroy_methods)
##destroy_methods << destroy_methods.to_a
end
def self.testing
return ##edit_methods
end
view_methods :index, :show
edit_methods :new, :create, :edit, :update
destroy_methods :destroy
end
The three methods above are different on purpose, just to show you what I've tried. The third one works, but returns the same results no matter what controller I test. Probably because the class variables are stored in the application controller so are changed globally.
Any help would be greatly appreciated.
The problem is that your class variables are inherited, but point to the same instance of Array. If you update one, it will also be updated on all classes that inherited the Array.
ActiveSupport offers a solution to this problem by extending the Class class with several methods to define inheritable class attributes. They are used everywhere internally in Rails. An example:
class ApplicationController
class_inheritable_array :view_method_list
self.view_method_list = []
def self.view_methods(*view_methods)
self.view_method_list = view_methods # view_methods are added
end
view_methods :index, :show
end
Now you can set default values in ApplicationController and override them later.
class MyController < ApplicationController
view_method :my_method
end
ApplicationController.view_method_list #=> [:index, :show]
MyController.view_method_list #=> [:index, :show, :my_method]
You can even use the view_method_list as an instance method on the controllers (e.g. MyController.new.view_method_list).
In your example you didn't define a way to remove methods from the lists, but the idea is to do something like the following (in case you need it):
# given the code above...
class MyController
self.view_method_list.delete :show
end
MyController.view_method_list #=> [:index, :my_method]
I turned it into a plugin like so:
module ThreatLevel
def self.included(base)
base.send :extend, ClassMethods
end
module ClassMethods
def roger_that!
class_inheritable_array :view_method_list, :edit_method_list, :destroy_method_list
self.view_method_list = Array.new
self.edit_method_list = Array.new
self.destroy_method_list = Array.new
def self.view_methods(*view_methods)
self.view_method_list = view_methods
end
def self.edit_methods(*edit_methods)
self.edit_method_list = edit_methods
end
def self.destroy_methods(*destroy_methods)
self.destroy_method_list = destroy_methods
end
view_methods :index, :show
edit_methods :new, :create, :edit, :update
destroy_methods :destroy
end
end
end
ActionController::Base.send :include, ThreatLevel
Calling roger_that! on the super_controller where you want it to take effect does the trick.