I have a project that's set up with the following models. Each -> represents a has_many relation:
Users->Goals->Milestones
My routes for the Milestones look like this:
user_goal_milestones GET /users/:user_id/goals/:goal_id/milestones(.:format) milestones#index
POST /users/:user_id/goals/:goal_id/milestones(.:format) milestones#create
new_user_goal_milestone GET /users/:user_id/goals/:goal_id/milestones/new(.:format) milestones#new
edit_user_goal_milestone GET /users/:user_id/goals/:goal_id/milestones/:id/edit(.:format) milestones#edit
user_goal_milestone GET /users/:user_id/goals/:goal_id/milestones/:id(.:format) milestones#show
PUT /users/:user_id/goals/:goal_id/milestones/:id(.:format) milestones#update
DELETE /users/:user_id/goals/:goal_id/milestones/:id(.:format) milestones#destroy
I find myself in many of the "functions" in the Milestones controller doing a lot of this:
def index do
#user = User.find(params[:user_id])
#goal = Goal.find(params[:goal_id])
end
def edit do
#user = User.find(params[:user_id])
#goal = Goal.find(params[:goal_id])
end
How can I modify my controller so I don't have to define #user and #goal all the time?
I tried putting them directly at the top, right after the start of the class definition block, but it didn't work.
If the params are always the same you can create a method like this
def set_user_and_goal
#user = User.find(params[:user_id])
#goal = Goal.find(params[:goal_id])
end
and put it in a before_filter at the top
before_filter :set_user_and_goal
and set it to whatever action you like
before_filter :set_user_and_goal, :only => [:edit, :index]
Edit:
Also, to make sure that it doesn't blow up in your face, you can do
#user = params.has_key?(:user_id) ? User.find(params[:user_id]) : nil
and as requested.. make sure that the goal belongs to the user by doing something like
#goals = #user.goals.find(params[:goal_id])
you an always define your own helper methods
def goal_milestone(goal)
user_goal_milestone(goal.user, goal)
end
You can add it to your application_helper, and then use any in any of your views. This would create the small helper methods as you asked in your question.
looking for a gem that does this for you didn't show me anything, but you can code this yourself in a generic way.
Related
I'm currently working on a personal project but stuck while trying to figure out the best way to hook into a particular point in the request cycle.
So what I want to accomplish is: from the "new" action (inside the child object's controller), I want to delete the associated parent object if the user lands up anywhere else but the "create" action as the next action. So far I'm thinking I want to write a private method that calls destroy on the parent object as a conditional if next action is anywhere else but "create" but I can't seem to find the right callback that precisely fits my particular situation. Can anyone suggest a good solution?
Here's my code to the respective controllers.
Parent Controller:
class EncountersController < ApplicationController
before_action :verify_if_logged_in, :authorize_provider, :set_patient_by_id, :view_all_patient_encounters
def index
#encounter = Encounter.new
end
def create
#encounter = Encounter.new(encounter_params)
#encounter.provider = current_user
#encounter.patient = #patient
if #encounter.save
redirect_to new_patient_encounter_soap_path(#patient, encounter)
else
render :index
end
end
private
def set_patient_by_id
#patient = Patient.find_by(id: params[:patient_id])
end
def view_all_patient_encounters
#encounters = #patient.encounters
end
def encounter_params
params.require(:encounter).permit(:encounter_type)
end
end
Child Controller:
class SoapsController < ApplicationController
before_action :set_patient_by_id, :set_encounter_by_id
def new
#soap = SOAP.new
end
def create
#If I don't end up here immediately after the new action for whatever reason
#I want to destroy the newly created parent object
end
private
def set_patient_by_id
#patient = Patient.find_by(id: params[:patient_id])
end
def set_encounter_by_id
#encounter = Encounter.find_by(id: params[:encounter_id])
end
end
Once I get to the new action of my SoapsController from the create action of my EncountersController, I want to destroy the associated Encounter object if I don't end up in Soaps#create as my next action.
In my project I am declaring instance variable with the help of before_action callback, but some of my controllers have same callback code. ex:
my golf_courses_users_controller.rb looks like:
private
def require_user_club_and_golf_course
#club_admin_user = User.find(params[:user_id])
#club = Club.find(params[:club_id])
#golf_course = GolfCourse.find(params[:golf_course_id])
end
my course_holes_controller.rb looks like:
private
def initialize_objects
#user = User.find(params[:user_id])
#club = Club.find(params[:club_id])
#golf_course = GolfCourse.find(params[:golf_course_id])
end
An easy way to set this up is to have the method assigning the instance variables in a parent class (for the sake of simplicity, I'll use ApplicationController here), and then call the before_action in the relevant controllers.
application_controller.rb
def initialize_objects
#user = User.find(params[:user_id])
#club = Club.find(params[:club_id])
#golf_course = GolfCourse.find(params[:golf_course_id])
end
golf_courses_users_controller.rb
before_action :initialize_objects
course_holes_controller.rb
before_action :initialize_objects
Depending on how widespread the use of this before action will be, you could even move the before_action to the same parent controller, and skip this where not needed:
application_controller.rb
before_action :initialize_objects
a_controller_not_using_the_before_action.rb
skip_before_action :initialize_objects
That seems dry, clean and pretty conventional to me - let me know what you think and if you have any questions.
I would produce a helper and use it everywhere:
module DbHelper do
def self.user_club_course(params)
# maybe raise unless
# params.values_at(*%i[user_id club_id golf_course_id]).none?(&:nil?)
[
User.find(params[:user_id]),
Club.find(params[:club_id]),
GolfCourse.find(params[:golf_course_id])
]
end
end
And use it like:
#user, #club, #golf_course = DbHelper.user_club_course(params)
I am trying to assign the author of a post automatically when he posts or edits. I have defined a helper function in application_controller.rb which checks for the current user and returns the object if signed in. But I get this error, and I don't seem to understand what's going wrong.
application_controller.rb
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
My controller action:
def create
#article=Article.find(params[:article_id])
#review=#article.reviews.assign_author(current_user.name).create(review_params)
redirect_to article_path(#article)
end
My model:
def self.assign_author(name)
self.author=name
end
Can anyone point what is the mistake I am making? I am sure its pretty small, but I don't seem to get it.
#review = #article.reviews.assign_author(current_user.name).create(review_params)
This is not how you add a new element to a collection. Should be something like this
#review = #article.reviews.build(review_params)
#review.assign_author(current_user.name)
#review.save
And assign_author method should be instance method (no self.)
def assign_author(name)
self.author = name
end
#article.reviews returns a collection of reviews, so you have to apply the author for all of them:
#review=#article.reviews.map{ |review| review.assign_author(current_user.name) }.create(review_params)
It seems like you need to restructure a bit.
#review=#article.reviews.assign_author(current_user.name).create(review_params)
is not something I typically work with. Instead, I would do something like this:
def create
#article=Article.find(params[:article_id])
review=Review.create(review_params)
review.assign_author(current_user.name)
#article.add_review(review)
redirect_to article_path(#article)
end
That's how I would normally expect a create method to look.
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.
I am fairly new to Ruby on Rails and as a C# developer, when I want to re-use code (for a repository class), I could put it into a base class of type <T> to be able to do something like this:
public virtual IEnumerable<T> GetAll()
{
return Context<T>.GetAll();
}
If I need to do any custom logic, I could, of course, override the method in my 'User' repository.
In Ruby, I am familiar that you can do this:
class UsersController < ApplicationController
This will allow access to all methods in ApplicationController and it's parent classes. When using scaffolding, it generates the following method in each of my child classes:
def index
#users = User.all
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #users }
end
end
What I end up with is 10 classes that have the same method, but the only difference is 'User.all', 'Post.all', etc.
How would I make this method generic so I can put it in my ApplicationController class?
Thanks for any assistance you can provide to a Ruby on Rails newbie.
The first thing to realize about the scaffolding code is that it can be abreviated, as such:
def index
#users = User.all
end
unless you intend to deliver the view in another format, like json, html, pdf, the respond_to block is unnecessary. If you still feel the need to dry up this method, you could do something like
# app/controllers/concerns/autoload_records.rb
module AutoloadRecords
included do
before_action :load_records, only: :index
before_action :load_record, only: [:create, :show, :edit, :update, :destroy]
end
private
def load_records
#records = model_class.all
end
def load_record
#record = model_class.find(params[:id])
end
def model_class
klass = self.class.to_s[/\A(\w+)sController\Z/,1] #=> get the name of the class from the controller Constant
Object.const_get(klass)
end
end
and write your controller like
class UsersController < ApplicationController
include AutoloadRecords
def index
#records # => #<ActiveRecord::Relation[...]>
end
def show
#record # => #<User ...>
end
def non_rest_action
#record # => nil
#records # => nil
end
end
Rather than doing an eval where you really don't want to be doing one. Check out Jose Valim's Inherited Resources gem. It provides the standard CRUD methods for all of your controllers and is quite sophisticated. It is also thoroughly tested so you don't have to worry about making sure your generic code operates as expected in all cases.
For details on how to use it see the GitHub page linked.
Maybe a simple solution could be to rely on mixins.
You define a module,
module MyModule
def my_index(klass)
#elements = klass.all
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #elements }
end
end
end
Then, you have in your controller,
include MyModule
def index
my_index(User)
end
Of course, you need to use #elements in your views. If you want a different variable name in each view you can do
def my_index(klass, var_name)
self.instance_variable_set(var_name, klass.all)
...
end
There are several rails plugins that help to reduce this kind of duplication. This one was covered in railscast episode 230.
https://github.com/josevalim/inherited_resources
Based on my experience, you rarely end up with 10 index action looking like #user = User.all. If you know in advance that some actions between different models will be identical - well then may be it makes sense to extract common logic. But then again may be these models are somehow connected? I wouldn't say in advance that Post and User will have identical index actions.
For a short method like this I wouldn't try to eleminate repetition because you may end up losing readability.