I have a somewhat bizarre requirement for a new Rails application. I need to build an application in which all routes are defined in multiple namespaces (let me explain). I want to have an application in which school subjects (math, english, etc) are the namespaces:
%w[math english].each do |subject|
namespace subject.to_sym do
resources :students
end
end
This is great and it works but it requires me to create a namespaced StudentsController for each subject which means if I add a new subject then I need to create a new controller.
What I would like is to create a Base::StudentsController and if, let's say the Math::StudentsController exists then it will be used and if it doesn't exist, then we can dynamically create this controller and inherit from Base::StudentsController.
Is this something that is possible? If so then how would I go about implementing this?
With routes defined this way:
%w[math english].each do |subject|
scope "/#{subject}" do
begin
"#{subject.camelcase}::StudentsController".constantize
resources :students, controller: "#{subject}::students", only: :index
rescue
resources :students, controller: "base::students", only: :index
end
end
end
rake routes outputs:
students GET /math/students(.:format) base::students#index
GET /english/students(.:format) english::students#index
if english/students_controller.rb is present and math/students_controller. is absent.
To restate your requirements:
Minimal declarations per subject/resource pair
Use dedicated controller (Math::StudentsController) if it exists, otherwise use base controller (StudentsController)
Rails expects each route to have a dedicated controller, and doesn't really have a good way to support the second requirement. So, this is how I would do it:
Dynamicroutes::Application.routes.draw do
SUBJECTS = [ "math", "english", "chemistry" ]
RESOURCES = [ "assignments", "students" ]
class DedicatedSubjectResourceControllerConstraint
def initialize(subject, resource)
#subject = subject
#resource = resource
end
def matches?(request)
begin
defined?("#{#subject.capitalize}::#{#resource.capitalize}")
return true
rescue NameError
Rails.logger.debug "No such class: #{#subject.capitalize}::#{#resource.capitalize}"
return false
end
end
end
class ValidSubjectConstraint
def matches?(request)
return SUBJECTS.include?(request.path_parameters[:subject])
end
end
SUBJECTS.each do |subject|
RESOURCES.each do |resource|
namespace subject, :constraints => DedicatedSubjectResourceControllerConstraint.new(subject, resource) do
resources resource
end
end
end
RESOURCES.each do |resource|
scope "/:subject", :constraints => ValidSubjectConstraint.new do
resources resource
end
end
end
This sounds like a use for const_missing. If what you want to do is
to create a Base::StudentsController
and if, let's say the Math::StudentsController exists
then it will be used
and if it doesn't exist, then we can dynamically create this controller and inherit from Base::StudentsController
You can achieve that by adding dynamic constant lookup (const_missing) and dynamic constant definition with inheritance (Object.const_set).
I imagine something like this; with a few tweaks and more rigorous checking, would work:
# initializers/dynamic_controllers.rb
class ActionDispatch::Routing::RouteSet
SUBJECTS = [ "math", "english", "chemistry" ]
def const_missing(name, *args, &block)
if SUBJECTS.any?{ |subject| name.include? subject.uppercase }
Object.const_set name, Class.new(Base::StudentsController)
else
super
end
end
end
That'll add dynamic constant lookups to ActionDispatch::Routing::RouteSet, from which Dynamicroutes::Application.routes inherits, so undefined constants in Dynamicroutes::Application.routes.draw will generate the corresponding classes subclassed from Base::StudentsController.
I believe this will do it:
%w[math english].each do |subject|
namespace subject.to_sym do
resources :students
end
end
match ':subject/students(/:action(/:id))' => 'base/students'
With these combined routes, /math/students goes to the Math::StudentsController, /english/students/ goes to the English::StudentsController, and all other subjects (e.g. /physics/students and /cs/students) go to the Base::StudentsController.
Which I think is exactly what you want and only adds one line of code to your original solution.
All the routing helpers like resources, scope, etc are just functions inside your application's routes. You could just define a custom function as follows:
YourApplication.routes.draw do
# Let's define a custom method that you're going to use for your specific needs
def resources_with_fallback(*args, &block)
target_module = #scope[:module].camelize.constantize
target_controller = "#{args.first.to_s}_controller".camelize
fallback_controller = args.last.delete(:fallback).to_s.camelize.constantize
# Create the target controller class
# using fallback_controller as the superclass
# if it doesn't exist
unless target_module.const_defined?(target_controller)
target_module.const_set target_controller, Class.new(fallback_controller)
end
# Call original resources method
resources *args, &block
end
# Now go ahead and define your routes!
namespace "test" do
namespace "new" do
# Use our custom_resources function and pass a fallback parameter
custom_resources :photos, :fallback => 'base/some_controller'
end
end
end
I tested this in Rails 3.2, but it should equally work well in all 3.x versions.
I included no null checks or begin/rescue blocks anywhere. Since you're going to use this custom function only when required, I'm assuming that you will pass the correct and necessary parameters. If say you passed a fallback controller that doesn't exist, I'd rather that the routes parsing fail with an exception, rather than trying to handle it.
Edit: Typo in function arguments
Edit 2: Forgot &block in function arguments
Edit 3: Add "_controller" to the target_controller variable
I ended up writing some custom logic into ActionDispatch::Routing::RouteSet::Dispatcher.controller_reference. I attempt to look up all of the constants required for the given controller and create them if they're missing. This code is FAR from perfect so please feel free to edit w/ improvements.
class ActionDispatch::Routing::RouteSet::Dispatcher
private
def controller_reference(controller_param)
const_name = #controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
obj = Object
const_name.split('::').each do |cn|
begin
obj = obj.const_get(cn)
rescue
if obj == Object
obj = obj.const_set(cn, Class.new(ApplicationController))
else
puts "Creating #{obj}::#{cn} based on Generic::#{cn}"
obj = obj.const_set(cn, Class.new("Generic::#{cn}".constantize))
end
end
end
ActiveSupport::Dependencies.constantize(const_name)
end
end
Related
I'm having a issue with scopes.
I've defined
# routes.rb
resources :asientos, module:'asientos'
# app/models/asientos/asiento.rb
module Asientos
class Asiento < ActiveRecord:Base
end
end
# app/controllers/asientos/asientos_controller.rb
module Asientos
class AsientosController < ApplicationController
def new
#asiento = Asientos::Asiento.new
end
end
end
# app/views/asientos/asientos/new
<%= form_for(#asiento) do |f| %>
...
rake routes
asientos GET /asientos(.:format) asientos/asientos#index
POST /asientos(.:format) asientos/asientos#create
new_asiento GET /asientos/new(.:format) asientos/asientos#new
edit_asiento GET /asientos/:id/edit(.:format) asientos/asientos#edit
asiento GET /asientos/:id(.:format) asientos/asientos#show
PATCH /asientos/:id(.:format) asientos/asientos#update
PUT /asientos/:id(.:format) asientos/asientos#update
DELETE /asientos/:id(.:format) asientos/asientos#destroy
Now whenever the form tries to render, i get
undefined method `asientos_asiento_index_path' for #<#<Class:0x000000065b3b40>:0x00000006ba5f30>
I've seen some of the answers like
form_for and scopes, rails 3
Module route in Rails with form_for(#object)
But none of them present a clear solution, o suggest some kind of patching.
Furthermore, form_for now generates asientos_ prefix, and in my controller now i have to rename also params.require(:asientos) to params.require(:asientos_asientos) ... not pretty...
Any suggestions (besides undoing namespacing) would be much appreciated. Thanks in advance.
Edit
It seems like by adding the following to the module definition, the route is generated as expected, without the "scope"
module Asientos
def self.use_relative_model_naming?
true
end
...
end
But it still wants an asientos_index_path... "undefined method `asientos_index_path'"
Well, after diving into tons of articles, and code i found the answer and as some posts pointed it has to do with Inflections.
My app has custom inflections for spanish, in which inflections look like:
Usuario -> Usuarios
Asiento -> Asientos
ItemAsiento -> ItemsAsiento
you'll notice it pluralizes the first word instead. That being said, the following piece of code from rails source extracted from rails/activemodel/lib/active_model/naming.rb shows the internals of whats happening
def initialize(klass, namespace = nil, name = nil)
#name = name || klass.name
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if #name.blank?
#unnamespaced = #name.sub(/^#{namespace.name}::/, "") if namespace
#klass = klass
#singular = _singularize(#name)
#plural = ActiveSupport::Inflector.pluralize(#singular)
#element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(#name))
#human = ActiveSupport::Inflector.humanize(#element)
#collection = ActiveSupport::Inflector.tableize(#name)
#param_key = (namespace ? _singularize(#unnamespaced) : #singular)
#i18n_key = #name.underscore.to_sym
#route_key = (namespace ? ActiveSupport::Inflector.pluralize(#param_key) : #plural.dup)
#singular_route_key = ActiveSupport::Inflector.singularize(#route_key)
#route_key << "_index" if #plural == #singular
end
Since my class is under Asientos namespace, the #name becomes Asientos::Asiento, #unnamespaced = "Asiento", #singular = "asientos_asiento", #plural = "asientos_asiento" and here lays the issue. #route_key gets suffixed if plural and singular are both equal.
But why are those equal? Since Spanish inflections pluralize first word, and it does not know of namespaces, the "asientos_asiento" is considered plural (true from an inflections stand point but wrong since first part is namespace not model name).
I guess here conventions played me, since convetion seems to assume last part is always the model name, thus english plural will always work just fine.
This should not happen since rails is already detecting the namespace, and it should not rely on the #name itself but strip the namespace and then singularize and pluralize without the namespace.
Monkey patch... here we go...
Thanks to everybody.
you need to specify the module/namespace in the form_for helper like this:
<% form_for [:asientos, #asiento] do |f| %>
I want to expose my database ids and encode/decode the id with routes helper. For encoding I use Hashids gem.
Now I have:
routes.rb
get 'companies/:id/:year', to: 'company#show', as: 'companies'
company url:
/companies/1/2015
For id encoding I have encode/decode helper methods:
def encode(id)
# encode...
return 'ABC123'
end
def decode(hashid)
# decode...
return 1
end
How I can implemented, that id will be with routes helper converted?
So must show the URL:
/companies/ABC123/2015
and controller must get automatically params with id 1.
Thanks for your answers! But I wont to decode params id without changes in the model or controller. After long consideration, I have decided the params id to manipulate, before controller get params. I manipulate params in routes Constraints.
example helper:
encoding_helper.rb
module EncodingHelper
def encode(id)
# encode...
return 'ABC123'
end
def decode(hashid)
# decode...
return 1
end
end
Create path with a encode id:
companies_path(id: encode(1), year: 2015) # => /companies/ABC123/2015
Manipulate params in routes Constraints:
lib/Constraints/decode_company_id.rb
module Constraints
class DecodeId
extend EncodingHelper
def self.matches?(request)
request.params['id'] = decode(request.params['id']).to_s if request.params['id'].present?
true
end
end
end
config/routes.rb
constraints(Constraints::DecodeId) do
get 'companies/:id/:year', to: 'company#show', as: 'companies'
end
After decode params id with constraints und without manipulation in controller, params id is 1.
You can use the to_param method for this.
#in Company
def to_param
self.encoded_id
end
def encoded_id
self.class.encode_id(self.id)
end
def find_by_encoded_id(encoded_id)
self.find_by_id(self.class.decode_id(encoded_id)
end
#class methods
class << self
def encode_id(id)
#encoding algorithm here
end
def decode_id(encoded_id)
#decoding algorithm here
end
end
this will mean that urls featuring the id of the company will actually use the encoded_id instead, assuming you pass through the company object to a path helper, eg company_path(#company).
Then, in your companies controller, you just need to make sure that you find_by_encoded_id(params[:id]) rather than find_by_id(params[:id]).
Rails router shouldn't be doing any decoding:
The Rails router recognizes URLs and dispatches them to a controller's action.
The logic should belong to the controller.
When your controller receives an encoded response:
#Appropriate controller
def show
Company.decode(params[:id])
end
This work work nicely if you slightly adjust your model method to:
def self.decode(code)
# decode => get id
find(id) #returns Company object
end
you can try this. custom method of friendly id
in model
extend FriendlyId
friendly_id :decode
# Try building a slug based on the following fields in
# increasing order of specificity.
def decode
conditional_check(self.id)
end
private
def conditional_check(id)
return "ABC123" if id == 1
end
Let's say I have two models (Model1 and Model2) that share the same controller, both have many instances of Model3.
How can I achieve to nest Model 3 within both models and have the route: model_3_path(#model) instead of model_1_model_3_path(#model) and model_2_model_3_path(#model)
I want my model_3_path(#model) function to look like this:
def model_3_path(model)
if model.is_a? Model1
"/model1/#{model.id}/model3"
elsif model.is_a? Model2
"/model2/#{model.id}/model3"
end
end
My current progress:
concern :three { resources :model3, shallow: true }
resources :model1, concerns: :three
resources :model2, concerns: :three, controller: :model1, except: [:index] # /model2 isn't permitted
I can't seem to find the right approach...
I found a simple solution: First, I removed the shallow in model3.By opening the helper class and adding a method_missing definition, this was easily possible:
def method_missing(method, *args, &block)
super unless /model3s?_(path|url|uri)$/ =~ method.to_s
sub_string = nil
if args.first.is_a? Model1
substring = 'model1'
elsif args.first.is_a? Model2
substring = 'model2'
end
self.send(method.to_s.gsub('model3', "#{substring}_model3"), *args, &block)
end
It would be possible to define each of those by themselves (new_model3_path, model3_path, edit_model3_path, model3s_path) but I found this one more concise.
If you want to have a path which does not specify it's parent just have it as a top level route, not a concern.
I'm trying to figure out a clean and dry way to extend the url_for such that it provides a dynamically based default parameter. I know that sounds weird, let me explain:
I have a set of nested resource routes that sit beneath a dynamic scope:
scope "/:network", constraints: {:network => /[^\/]+/} do
constraints DomainConstraint.new do
resources :users do
resources :posts
end
end
end
This gives routes like:
/mysite.com/users/mike
/someothersite.com/users/sally
In order to generate these routes in a view, I can easily do:
mike = User.find_by_name("mike")
sally = User.find_by_name("sally")
user_path(mike.network, mike)
user_path(sally.network, sally)
However, this does not seem very DRY to me as the network is fixed for each user. I'd rather be able to concisely say:
user_path(mike)
So, I've come up with a solution that works, but it seems very hacky and I'm wondering if there is a more "proper" way to do it:
module UrlForWithDefault
def self.included(base)
base.module_eval do
alias_method_chain :url_for, :default
end
end
def url_for_with_default(*args)
if args[0].kind_of?(Hash)
opts = args[0]
if opts.has_key?(:_positional_keys) and opts[:_positional_keys].include?(:network)
opts[:_positional_args].unshift(opts[:_positional_args][0].network)
url_for_without_default(opts)
end
end
url_for_without_default(*args)
end
end
ActionDispatch::Routing::UrlFor.send(:include, UrlForWithDefault)
I don't like this approach because it hacks the ":_positional_keys" and ":_positional_args" attributes, which could potentially change.
could not url_options or Rails.application.routes.default_url_options do what you want?
Have STI classes:
class Page < ActiveRecord::Base
belongs_to :user
end
class FirstTypePage < Page
end
class SecondTypePage < Page
end
Controllers for each class,
class PageController < AplicationCorroller
end
class FirstTypePageController < PageController
end
class SecondTypePageController < PageController
end
And routings:
resources :user
resource :page
end
How to handle FirstTypePage by FirstTypePageController, SecondTypePage by SecondTypePageController on single path?
i.e.
user/1/page/2 is handled by:
FirstTypePageController if "page 2" type is "FirstTypePage",
and by SecondTypePageController if "page 2" type is "SecondTypePage" ?
UPDATE: My solution:
match 'user/:user_id/page/:action',
:controller=>'page/first_type_page',
:constraints=>PageConstraints.new('FirstTypePage')
match 'user/:user_id/page/:action',
:controller=>'page/second_type_page',
:constraints=>PageConstraints.new('SecondTypePage')
class PageConstraints
##cache ||= {}
def initialize o_type
##mutex = Mutex.new
#o_type = o_type
end
def matches?(request)
user_id = request.params[:user_id]
#add Mutex lock here
unless page_type = ##cache[user_id]
page_type = User.find(user_id).do_some_magik_to_suggest_type
##cache[page_id] = page_type
##cache.shift if ##cache.size > 1000
end
page_type == #o_type
end
end
I think this solution will work fast on a small amount of page types, and we can manage memory size, used for routings on a large amount of pages
I can see one option to do that - preload all pages in the routes.rb and define special routes for each page.
resources :users do |user|
Page.all do |page|
if page.first_type?
# ... routes to first_type_page_controller
else
# ...
end
end
Another solution could be to use strategy patter in the PageController (no need to use FirstTypePageController and other).
pages_controller.rb:
before_filter :choose_strategy
def show
#strategy.show
end
private
def choose_strategy
#strategy = PagesControllerStrategy.new(self, page)
end
def page
#page ||= Page.find params[:id]
end
pages_controller_strategy.rb:
class PagesControllerStrategy
def initialize(controller, page)
#controller = controller
#page = page
end
def show
# do what you what with controller and page
end
end
However, I'd suggest you to split the behavior on the view level only:
show.html.haml:
- if page.first_type?
= render 'pages/first_type'
- else
// ...
EDIT:
I just found another solution, that could help you - custom constraints.
http://railsdispatch.com/posts/rails-3-makes-life-better
I'm not sure if that works in your case, but I think it is worth to play with routes more.
you can do it with before_filter, but separating STI models into different controllers isn't good solution. I totally agree with next quote
This may not always apply, but I have yet to see a case where STI works well with multiple controllers. If we are using STI, our objects share a set of IDs and attributes, and therefore should all be accessed in basically the same way (find by some attribute, sort by some attribute, restrict to administrators, etc). If presentation varies greatly we may want to render different model-specific views from our controller. But if object access varies so much that it suggests separate controllers, then STI may not have been the correct design choice.
took here http://code.alexreisner.com/articles/single-table-inheritance-in-rails.html