This is going to sound strange, but hear me out...I need to be able to make the equivalent of a POST request to one of my other controllers. The SimpleController is basically a simplified version of a more verbose controller. How can I do this appropriately?
class VerboseController < ApplicationController
def create
# lots of required params
end
end
class SimpleController < ApplicationController
def create
# prepare the params required for VerboseController.create
# now call the VerboseController.create with the new params
end
end
Maybe I am over-thinking this, but I don't know how to do this.
Inter-controller communication in a Rails app (or any web app following the same model-adapter-view pattern for that matter) is something you should actively avoid. When you are tempted to do so consider it a sign that you are fighting the patterns and framework your app is built on and that you are relying on logic has been implemented at the wrong layer of your application.
As #ismaelga suggested in a comment; both controllers should invoke some common component to handle this shared behavior and keep your controllers "skinny". In Rails that's often a method on a model object, especially for the sort of creation behavior you seem to be worried about in this case.
You shouldn't be doing this. Are you creating a model? Then having two class methods on the model would be much better. It also separates the code much better. Then you can use the methods not only in controllers but also background jobs (etc.) in the future.
For example if you're creating a Person:
class VerboseController < ApplicationController
def create
Person.verbose_create(params)
end
end
class SimpleController < ApplicationController
def create
Person.simple_create(params)
end
end
Then in the Person-model you could go like this:
class Person
def self.verbose_create(options)
# ... do the creating stuff here
end
def self.simple_create(options)
# Prepare the options as you were trying to do in the controller...
prepared_options = options.merge(some: "option")
# ... and pass them to the verbose_create method
verbose_create(prepared_options)
end
end
I hope this can help a little. :-)
Related
Lets say that we have a rails app which has a sidebar on every page that shows some data, like post archives, post categories etc. Which is the best way to share the same data on each of our controllers?
Iguess the most easy fix is to use the same before_actions on each controller, but this doesn't DRY up much our code, or maybe move all these into a parent class/cotrnoller that all controllers will inhert from, but is there a better way of doing this?
Simple Solution:
Normally, I just put these into application_controller.rb as a before_action.
Example
# app/controllers/application_controller.rb
before_action :set_sidebar_resources
# ...
private
def set_sidebar_resources
#sidebar_archives = Archive.all
#sidebar_categories = Category.all
end
Modular Solution:
Simple solution above works great until you define more and more methods and other global controller logic into ApplicationController, and then the file becomes too big to manage. The following is a less conventional approach favouring more of manageability rather than simplicity.
Example
# app/controllers/application_controller.rb
include WithSidebar
# app/controllers/concerns/with_sidebar.rb
module WithSidebar
extend ActiveModel::Concern
included do
before_action :set_sidebar_resources
private
def set_sidebar_resources
#sidebar_archives = Archive.all
#sidebar_categories = Category.all
end
end
end
Rails controllers inherit from application controller.
ex.
class ClientsController < ApplicationController
You could add before_actoion to ApplicationController that pulls the data you want into #variable and then use that variable in other controllers.
Just don't over-do it and keep your controllers skinny :)
I'm slightly new to Rails (i.e. stupid and need some teachin').
I have a controller (call it ControllerFoo) that performs a particular task (theMethod) which could be useful in other controllers (say, from within ControllerBar). So, of course, the method is defined as self.theMethod in ControllerFoo (which means it's a class method, right?), and access in ControllerBar as ControllerFoo.theMethod. Confused yet?
Here's the problem: the ControllerFoo.theMethod uses session data, and when called from ControllerBar, session is nil. In fact, it seems that session is also nil when being called from itself. I guess what I'm saying is class methods can't access session data?
<rant>I hate how session data can't simply be accessed anywhere like in PHP</rant>
So for now, since I'm not smart enough to know how to do this correctly, I've just duplicated the logic in several places throughout my app. But this is not DRY at all, and I hate it.
So how can I create a method in a controller that's accessible to other controllers and can also access session data?
class ControllerFoo < ApplicationController
def self.theMethod (greeting)
p "#{greeting} #{session[:user]}!"
end
end
class ControllerBar < ApplicationController
def show
ControllerFoo.theMethod("Hello,")
end
end
Couple of options...
Put the shared method in the shared parent ApplicationController
Create a module that both ControllerFoo and ControllerBar will include
e.g.
module SharedModule
def theMethod (greeting)
p "#{greeting} #{session[:user]}!"
end
end
class ControllerFoo < ApplicationController
include SharedModule
end
class ControllerBar < ApplicationController
include SharedModule
def show
theMethod("Hello,")
end
end
The way you would do this is Ruby would be to create a module containing the class (or instance) methods you wish to share and include it in the classes you need to have those methods defined in.
I understand that we're supposed to avoid putting logic in the controller. So what is the proper way to implement things like user access controls. Let's suppose I have User, where each instance has a flag method admin? that determines whether the user can access information from other users.
Option 1: Put access controls in custom model
Model:
class User < ActiveRecord::Base
def self.get_list(accessor)
return [] unless accessor.admin?
self.all
end
end
Controller:
class UsersController < ApplicationController
def index
#users = User.get_list(current_user)
end
end
current_user would probably be defined somewhere in the application controller.
Option 2: Put access controls in the controller
Model:
class User < ActiveRecord::Base
end
Controller:
class UsersController < ApplicationController
def index
#users = current_user.admin? User.all : []
end
end
There are also some peripheral consequences such as where tests go and how they're implemented.
My instinct is that the first of the two options is preferable, but I've only ever used option 2 in the past. Also, it seems like the generally accepted (as far as I can tell) practice of putting on action-wide access filters is done at the controller level as in:
class UsersController < ApplicationController
before_filter :verify_logged_in
end
Any logic controlling routing of your application belongs in your controller. As for your example, the first is preferable but really there's not much in it.
It's easy for opinionated frameworks make us almost obsessive about doing things in a perceived correct way. In your example such a tiny amount of logic would, in my opinion, be perfectly fine to leave in your controller. If you were to abstract it, give it a descriptive name that described better what the method is doing otherwise you're simply making your code needlessly difficult to read.
As a rails rookie, the second option to me seems far easier to read and comphrehend. I prefer how I can read the single line in the controller and see exactly what you are doing.
In the first example, this logic is hidden someplace else (obviously the model, but I'm a rookie remember!) and seems slightly verbose (for this particular example).
I don't mean to suggest keeping it easier for new kids is A Good Thing, just pointing out a preference.
I am in the middle of migrating my application from using subdirectories for userspace to subdomains (ie. domain.com/~user to user.domain.com). I've got a method in my user class currently to get the "home" URL for each user:
class User
def home_url
"~#{self.username}"
# How I'd like to do it for subdomains:
#"http://#{self.username}.#{SubdomainFu.host_without_subdomain(request.host)}"
end
end
I'd like to update this for subdomains, but without hardcoding the domain into the method. As you can see, I am using the subdomain-fu plugin, which provides some methods that I could use to do this, except that they need access to request, which is not available to the model.
I know it's considered bad form to make request available in a model, so I'd like to avoid doing that, but I'm not sure if there's a good way to do this. I could pass the domain along every time the model is initialized, I guess, but I don't think this is a good solution, because I'd have to remember to do so every time a class is initialized, which happens often.
The model shouldn't know about the request, you're right. I would do something like this:
# app/models/user.rb
class User
def home_url(domain)
"http://#{username}.#{domain}"
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def domain
SubdomainFu.host_without_subdomain(request.host)
end
# Make domain available to all views too
helper_method :domain
end
# where you need it (controller or view)
user.home_url(domain)
If there is such a thing as a canonical user home URL, I would make a configurable default domain (e.g. YourApp.domain) that you can use if you call User#home_url without arguments. This allows you to construct a home URL in places where, conceptually, the "current domain" does not exist.
While molf's answer is good, it did not solve my specific problem as there were some instances where other models needed to call User#home_url, and so there would be a lot of methods I'd have to update in order to pass along the domain.
Instead, I took inspiration from his last paragraph and added a base_domain variable to my app's config class, which is the set in a before_filter in ApplicationController:
module App
class << self
attr_accessor :base_domain
end
end
class ApplicationController < ActionController::Base
before_filter :set_base_domain
def set_base_domain
App.base_domain = SubdomainFu.host_without_subdomain(request.host)
end
end
And thus, when I need to get the domain in a model, I can just use App.base_domain.
In models and controllers, we often use Rails macros like before_validation, skip_before_filter on top of the class definition.
How is this implemented? How do I add custom ones?
Thanks!
They're just standard Ruby functions. Ruby's flexible approach to syntax makes it look better than it is. You can create your own simply by writing your method as a normal Ruby function and doing one of the following:
putting it somewhere that's accessible by your controllers such as application.rb
putting it in a file and requiring it in.
mixing the code into a class via the Ruby include keyword.
That last option is great for model classes and the first option is really only for controllers.
An Example
An example of the first approach is shown below. In this example we add code into the ApplicationController class (in application.rb) and use it in the other controllers.
class BusinessEntitiesController < ApplicationController
nested_within :Glossary
private
# Standard controller code here ....
The nested_within provides helper functions and variables to help identify the id of the "parent" resource. In effect it parses the URL on the fly and is accessible by every one of our controllers. For example when a request comes into the controller, it is automatically parsed and the class attribute #parent_resource is set to the result of a Rails find. A side effect is that a "Not Found" response is sent back if the parent resource doesn't exist. That saves us from typing boiler plate code in every nested resource.
That all sounds pretty clever but it is just a standard Ruby function at heart ...
def self.nested_within(resource)
#
# Add a filter to the about-to-be-created method find_parent_id
#
before_filter :find_parent_id
#
# Work out what the names of things
#
resource_name = "#{resource.to_s.tableize.singularize}"
resource_id = "#{resource_name}_id"
resource_path = "#{resource.to_s.tableize}_path"
#
# Get a reference to the find method in the model layer
#
finder = instance_eval("#{resource}.method :find_#{resource_name}")
#
# Create a new method which gets executed by the before_filter above
#
define_method(:find_parent_id) do
#parent_resource = finder.call(params[resource_id])
head :status => :not_found, :location => resource_path
unless #parent_resource
end
end
The nested_within function is defined in ApplicationController (controllers/application.rb) and therefore gets pulled in automatically.
Note that nested_within gets executed inside the body of the controller class. This adds the method find_parent_id to the controller.
Summary
A combination of Ruby's flexible syntax and Rail's convention-over-configuration makes this all look more powerful (or weirder) than it actually is.
Next time you find a cool method, just stick a breakpoint in front of it and trace through it. Ahh Open Source!
Let me know if I can help further or if you want some pointers on how that nested_within code works.
Chris
Chris's answer is right. But here's where you want to throw your code to write your own:
The easiest way to add Controller methods like that is to define it in ApplicationController:
class ApplicationController < ActionController::Base
...
def self.acts_as_awesome
do_awesome_things
end
end
Then you can access it from individual controllers like so:
class AwesomeController < ApplicationController
acts_as_awesome
end
For models, you want to reopen ActiveRecord::Base:
module ActiveRecord
class Base
def self.acts_as_super_awesome
do_more_awesome_stuff
end
end
end
I personally would put that in a file in config/initializers so that it gets loaded once, and so that I know where to look for it always.
Then you can access it in models like so:
class MySuperAwesomeModel < ActiveRecord::Base
acts_as_super_awesome
end