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}
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.
I have an email_template model that has a nested resource moves to handle moving an email_template from one folder to another.
However, I want to namespace these actions in a :templates namespace because I have several other resources that are template items as well.
Since I'm namespacing, I don't want to see templates/email_templates/:id in the URL, I'd prefer to see templates/emails/:id.
In order to accomplish that I have the following:
# routes.rb
namespace :templates do
resources :emails do
scope module: :emails do
resources :moves, only: [:new, :create]
end
end
end
Everything works fine when I do CRUD actions on the emails, since they are just using the :id parameter. However, when I use the nested moves, the parent ID for the emails keeps coming across as :email_id and not :email_template_id. I'm sure this is the expected behavior from Rails, but I'm trying to figure out how the parent ID is determined. Does it come from the singular of the resource name in the routes, or is it being built from the model somehow?
I guess it's ok to use templates/emails/:email_id/moves/new, but in a perfect world I'd prefer templates/emails/:email_template_id/moves/new just so developers are clear that it's an email_template resource, not a email.
# app/controllers/templates/emails_controller.rb
module Templates
class EmailsController < ApplicationController
def show
#email_template = EmailTemplate.find(params[:id])
end
end
end
# app/controllers/templates/emails/moves_controller.rb
module Templates
module Emails
class MovesController < ApplicationController
def new
# Would prefer to reference via :email_template_id parameter
#email_template = EmailTemplate.find(params[:email_id])
end
def create
#email_template = EmailTemplate.find(params[:email_id])
# Not using strong_params here to demo code
if #email_template.update_attribute(:email_tempate_folder_id, params[:email_template][:email_template_folder_id])
redirect_to some_path
else
# errors...
end
end
end
end
end
You could customize the parameter as:
resources :emails, param: :email_template_id do
...
end
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 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.
How do you setup your views, controllers and routes?
One controller for everything the control panel does, or many?
Firstly, let's try to think how we would view the various panels. Let's say our control panel is pretty simple. We have one panel to show all the users who have signed-up and can CRUD them, and another panel to show all of the images that have uploaded, and we can carry up CRUD on those too.
Routes:
scope path: 'control_panel' do
get 'users', to: 'panels#users', as: :panel_show_users
get 'photos', to: 'panels#photos', as: :panel_show_photos
end
Controller:
class PanelsController < ApplicationController
def users
#users = User.all
end
def photos
#photos = Photo.all
end
end
View file structure:
panels
|_ users.html.erb
|_ photos.html.erb
Okay, now I don't see any problems with that, to simply access the panels and populate the views with data. Do you see any problems?
Here is where I'm sort of at a cross roads though. What should I do when I want to Created Update and Delete a user/photo? Should I put them all in the PanelsController?
class PanelsController < ApplicationController
before_action :protect
def users
#users = User.all
end
def update_user
#user = User.find(params[:id])
#user.update(user_params)
end
def photos
#photos = Photo.all
end
def update_photo
#photo = Photo.find(params[:id])
#photo.update(photo_params)
end
private
def protect
redirect_to root_url, error: 'You lack privileges!'
end
end
While this would result in a large PanelsController, it would feel good to be able to execute that protect action and just one controller hook. It would also mean the routes would be easy to setup:
scope path: 'control_panel' do
get 'users', to: 'panels#users', as: :panel_show_users
post 'user', to: 'panels#update', as: :panel_create_user
get 'photos', to: 'panels#photos', as: :panel_show_photos
post 'photos', to: 'panels#photos', as: :panel_create_photo
end
I should use resource routes here?
Like I say, this will result in a huge panels controller, so I was thinking it may be better to have a separate controller for each resource and then redirect to panels views?
Routes:
scope path: 'control_panel' do
resources :users
resources :photos
end
Controllers:
class UsersController < ApplicationController
def index
end
def show
end
def new
end
def create
end
def update
end
def destroy
end
end
class PhotosController < ApplicationController
def index
end
def show
end
def new
end
def create
end
def update
end
def destroy
end
end
Still some quirks though. I have my Users#index action there, but what if I have two routes that return an index of all users? In the control panel, but also, when people are searching for another user, for example. Should I have two routes in the User controller? def public_users and def control_panel_users? That may be the answer. Could setup a hook to run #users = User.all in both of them, but redirect to a different location, and not have the protect method redirect them.
How should I protect these routes from non-admins? Should I move my protect method into the the application controller? Wouldn't this be a bit fiddly to setup?
class ApplicationController < ActionController
before_action :protect
def protect end
end
class StaticController < ApplicationController
skip_before_action [:home, :about, :contact]
def home
end
def about
end
def contact
end
end
But that is my question. 1 control panel controller or no control panel controller.
I really wish there was more advanced tutorials out there :( Billions of books on CRUD, MVC and things, but nothing on advanced things like control panels and AJAX...
Don't have a control panel controller. And to protect stuff from non-admins, use namespacing - read more about it here: http://guides.rubyonrails.org/routing.html#controller-namespaces-and-routing
You can protect your 'admin'-namespaced controllers with authentication, and have the non-namespaced controllers open to the public (or open to non-admin users)
With regards to your def public_users and def control_panel_users question, you could just have two def index methods - one in the non-namespaced controller, and one in the admin-namespaced controller. They would each do different things.
So, you'd have 4 controllers in total:
2 non-namespaced, one for users, one for photos (containing all public stuff)
2 admin-namespaced, one for users, one for photos (containing all control panel stuff)
If you wanted, rather than using 'admin' as the namespace, you could use some other term you prefer - like 'panel'. 'Admin' is pretty conventional though.