Generating generic paths for STI models in Rails 4 - ruby-on-rails

Suppose I have a Rails 4 application that manages Widget objects, and using Simple Table Inheritance I have specialisations Widget::Foo and Widget::Bar.
I would like to manage all my Widget objects through a single WidgetsController.
I have the following models:
class Widget < ActiveRecord::Base; end
class Widget::Foo < Widget
# Foo specific details...
end
class Widget::Bar < Widget
# Bar specific details...
end
And a simple controller:
class WidgetsController < ApplicationController
def index
#widgets = Widget.all
end
def show
#widget = Widget.find(params[:id])
end
end
My routes include
resources :widgets, only: [:index, :show}
In my index.html.haml I have something like:
- #widgets.each do |widget|
= link_to "View your widget!", [#widget]
Which is where everything goes wrong.
Between url_for and polymorphic_path Rails will attempt to find a widget_foo_path, rather than using the extant widget_path.
I would rather not add additional routes, or controllers, and I would prefer not to specify the url helper manually. Is there a way to tell Rails that Widget::Foo and Widget::Bar objects should be linked to using the widget_path helper?

I ended up resolving the issue by creating a mixin:
module GenericSTIRoutes
def initialize(klass, namespace = nil, name = nil)
super(klass, namespace, name)
superklass = klass
while superklass.superclass != ActiveRecord::Base
superklass = superklass.superclass
end
#param_key = _singularize(superklass.name)
#route_key = ActiveSupport::Inflector.pluralize(#param_key)
#singular_route_key = #param_key.dup
#route_key << "_index" if #plural == #singular
end
end
And then modifying Widget.model_name as follows:
def self.model_name
#_model_name ||= Class.new(ActiveModel::Name) do
include GenericSTIRoutes
end.new(self, nil)
end

Related

Route not find - Rails

Rails 3.2
In my controllers/admin/accounts_receivables_contoller.rb, I have:
class Admin::AccountsReceivables < Admin::ApplicationController
def index
...
end
and in one of the views, I have:
= link_to admin_accounts_receivables_path
In my config/routes.rb, I have:
namespace :admin do
resources :accounts_receivables do
collection do
get 'admin_report'
get 'customer_report'
post 'process_invoices'
end
end
end
rake routes, produces:
admin_accounts_receivables GET admin/accounts_receivables(.:format) admin/accounts_receivables#index
However, when I click on the link, I get (in the browser, but no entry in the log file):
uninitialized constant Admin::AccountsReceivablesController
I do not have a corresponding AccountsReceivable model, as I don't need it.
Any ideas?
The class should be named AccountsReceivablesController and you should nest the class explicitly instead of using the scope resolution operator so that it has the correct module nesting:
module Admin
class AccountsReceivablesController < ApplicationController
def index
# ...
end
end
end
When you use the scope resolution operator class Admin::AccountsReceivablesController - the module nesting is resolved to the point of definition which is Main (the global scope) and not Admin. For example:
module Admin
FOO = "this is what we expected"
end
FOO = "but this is what we will actually get"
class Admin::AccountsReceivablesController < Admin::ApplicationController
def index
render plain: FOO
end
end
See The Ruby Style Guide - namespaces.
class Admin::AccountsReceivables < Admin::ApplicationController
should be...
class Admin::AccountsReceivablesController < Admin::ApplicationController

Descriptive URL, different pages, but same controller ( domain.com/cars/wheels)

Custom routes for the same controller. I have many semi-static pages in my app (actually stored in my database with a group and page name field), they are grouped by product and then subject, for example
Cars: tires, Wheels, Radio, Windshield
Homes: Doors, Windows, Roof
Products and Services: data services
I would prefer not to make a new controller for each group. However, I am trying to get different URL paths that are descriptive. For example:
domain.com/cars/tires_and_spokes
domain.com/cars/wheels
domain.com/homes/doors_and_knobs
domain.com/homes/windows
domain.com/products_and_services/data_service
currently, all I have is
domain.com/pages/cars_tires_and_spokes
etc.
but I prefer the former.
Routes:
pages_from_DB =[
{group:"cars", name:"tires and spokes"}
{group:"cars", name:"wheels"}
{group:"homes", name:"tires and spokes"}
{group:"homes", name:"windows"}
]
pages = %w[
cars_tires_and_spokes
cars_wheels
homes_doors_and_knobs
homes_windows
products_and_services_data_service
]
pages.each do |page|
get page, controller: "pages", action: page
end
Controller:
class PagesController < ApplicationController
pages_from_DB =[
{group:"cars", name:"tires and spokes"}
{group:"cars", name:"wheels"}
{group:"homes", name:"tires and spokes"}
{group:"homes", name:"windows"}
]
pages = %w[
cars_tires_and_spokes
cars_wheels
homes_doors_and_knobs
homes_windows
products_and_services_data_service
]
pages.each do |page|
define_method(page) do
end
end
end
Looks like you've missed the point of nested resources:
#config/routes.rb
resources :groups, path: "", only: [] do
resources :pages, path: "" #-> url.com/:group_id/:id
end
This will direct any user to the pages controller, to which they're able to pull both the Group and ID from their respective models:
#app/controllers/pages_controller.rb
class PagesController < ApplicationController
def show
#group = Group.find params[:group_id]
#page = #group.pages.find params[:id]
end
end
--
This should be accompanied by the following models:
#app/models/group.rb
class Group < ActiveRecord::Base
has_many :pages
end
#app/models/page.rb
class Page < ActiveRecord::Base
belongs_to :group
end
If you wanted to treat the routes with a slug (instead of id), you'll want to look at friendly_id:
#Gemfile
gem "friendly_id"
$ rails generate friendly_id
$ rails generate scaffold group title:string slug:string:uniq
$ rails generate scaffold page title:string slug:string:uniq
$ rake db:migrate
#app/models/group.rb
class Group < ActiveRecord::Base
has_many :pages
extend FriendlyId
friendly_id :title, use: [:slugged, :finders]
end
#app/models/page.rb
class Page < ActiveRecord::Base
belongs_to :group
extend FriendlyId
friendly_id :title, use: [:slugged, :finders]
end
This will allow you to use:
<%= link_to group_pages_path(#group, #page) %>
# -> url.com/group-name/page-title
Update
The above code was based on the idea that you would be able to put your pages into the database (as you should). If you don't want to do that, there is a wildcard route you may be able to use:
#config/routes.rb
get "*group/:page", to: "pages#show"
If the pages were "semi-static" (still don't know what that means), you'd then be able to render the various views as required:
#app/controllers/pages_controller.rb
class PagesController < ApplicationController
def show
group = params[:group]
render "#{params[:group]}/#{params[:page]"
end
end
The above would give you the following link:
url.com/path/to/your/group/this-is-the-page-id
Depending on your group / sub-group structure, it should give you the ability to call the various views. I don't agree with it but it's apparently what you wanted.
--
Custom Middleware
We also created custom middleware which has some of the functionality for this:
#config/routes.rb
get "*group/:page", to: PageDispatcher.new
#app/controllers/pages_controller.rb
class PagesController < ApplicationController
cattr_accessor :pages #-> PagesController.pages
##pages = %w[
cars_tires_and_spokes
cars_wheels
homes_doors_and_knobs
homes_windows
products_and_services_data_service
]
end
#lib/page_dispatcher.rb
class PageDispatcher
#Init
def initialize(router)
#router = router
end
#Env
def call(env)
group = env["action_dispatch.request.path_parameters"][:group]
page = env["action_dispatch.request.path_parameters"][:page]
if PagesController.pages.include? page
strategy(slug).call(#router, env)
else
raise ActiveRecord::RecordNotFound
end
end
##########################################
private
#Strategy
def strategy(url)
Render.new(url)
end
####################
#Render
class Render
def initialize(url)
#url = url
end
def call(router, env)
controller = PagesController
action = "show"
controller.action(action).call(env)
end
end
####################
end

Global variable for language_id available in views and in models

I'm trying to share a session variable in both the controllers, the views and the model.
With the following code, it is working in the controllers and in the views :
class ApplicationController < ActionController::Base
protect_from_forgery
helper_method :best_language_id
# Returns the ID of the language to use to display the description.
def best_language_id
#best_language_id ||= session[:best_language_id]
#best_language_id ||= current_project.default_language.id
return #best_language_id
end
end
But I can't call it from the model.
I would like to be able to call best_language_id either in the controllers, views and in one model, to get a fallback of the best_language_id if a translation is not found.
Example in my model (not working) :
class Point < ActiveRecord::Base
# Retuns the attached word in the given language if exists.
# Otherwise, falls back on another translation
def word(preffered_language_id)
word = Word.find(:translation_id => self.translation_id, :language_id => preffered_language_id)
if word.blank?
word = translations.where(:translation_id => self.translation_id, :language_id => best_language_id)
end
return word
end
end
I know that model should not include applicationcontroller method calls, but how is it possible to share my best_language_id accross controllers and model ?
Edit : using i18n is not the question here. Translations are not fixed string but variables in a database.
Thanks for helping !
In your rails app, you have a base module in config/application.rb. It should be named after your application. Let's say its called MyApp. What you could do is define two methods like this:
module MyApp
...
def self.language_id=(value)
#language_id = value
end
def self.language_id
#language_id ||= 'en' # default vaule
end
...
end
Then, in app/controllers/application_controller.rb add a before_filter like this:
before_filter :language
def language
MyApp.language_id = session[:language_id] if session[:language_id]
end
Then, from all over the app, you can access the value via
MyApp.language_id
Needless to say that the approach is not thread safe so don't use it in a threaded environment.
I would suggest you switch the situation around, store the best_language_id in the model as a class accessor, then you can set and get it from your controllers and it will still be available in the models.
class Point < ActiveRecord::Base
cattr_accessor :best_language_id # to store the variable
end
# Persist the content of that variable at the start of every action
class ApplicationController < ActionController::Base
before_filter :set_best_language
def set_best_language
Point.best_language_id = session[:best_language_id]
Point.best_language_id ||= current_project.default_language.id
end
end
# Use the variable in a controller
class SomeOtherController < ActionController::Base
def show
#best_language = Language.find(Point.best_language_id)
...
end
end
# Use the variable in a model
class SomeOtherController < ActiveRecord::Base
def some_method
best_language = Language.find(Point.best_language_id)
...
end
end

Rails Controller in nested module cannot resolve model in module with the same

I have a rails model located at app/models/scheduling/availability.rb which looks like:
class Scheduling::Availability < ActiveRecord::Base
end
I have a Rails controller located at *app/controllers/admin/scheduling/availabilities_controller.rb* which looks like:
class Admin::Scheduling::AvailabilitiesController < ApplicationController
def index
#availabilities = Scheduling::Availability.all
end
end
My routes look like:
namespace :admin do
namespace :scheduling do
resources :availabilities
end
end
When trying to load the url:
/admin/scheduling/availabilities
I get the error:
uninitialized constant
Admin::Scheduling::AvailabilitiesController::Scheduling
I have a feeling this is because Rails is confusing the Scheduling module/namespaces.
What am I doing wrong?
Found my answer in another answer.
Need to preface my module with ::
class Admin::Scheduling::AvailabilitiesController < ApplicationController
def index
#availabilities = ::Scheduling::Availability.all
end
end

How is it possible the class inheritance in namespaces using Ruby on Rails 3?

In my RoR3 application I have a namespace called NS1 so that I have this filesystem structure:
ROOT_RAILS/controllers/
ROOT_RAILS/controllers/application_controller.rb
ROOT_RAILS/controllers/ns/
ROOT_RAILS/controllers/ns/ns_controller.rb
ROOT_RAILS/controllers/ns/profiles_controller.rb
I would like that 'ns_controller.rb' inherits from application controller, so in 'ns_controller.rb' file I have:
class Ns::NsController < ApplicationController
...
end
Is this the right approach? Anyway if I am in this situation...
In ROOT_RAILS/config/routes.rb I have:
namespace "ns" do
resources :profiles
end
#profile is a ActiveRecord:
#profile.find(1).name
=> "Ruby on"
#profile.find(1).surname
=> "Rails"
In application_controller.rb I have:
class ApplicationController < ActionController::Base
#profile = Profile.find(1)
end
In ns_controller.rb I have:
class Ns::NsController < ApplicationController
#name = #profile.name
#surname = #profile.surname
end
... #name and #surname variables are not set. Why?
Unless there's some code you're not showing here, you're trying to set an instance variable in a class body rather than an instance method, which means the variable won't be available in controller actions (which are instance methods).
If you want find method that can be inherited, you could do something like this:
class ApplicationController < ActionController::Base
def load_profile
#profile = Profile.find(params[:id])
end
end
class Ns::NsController < ApplicationController
before_filter :load_profile
def show
# #profile assigned a value in load_profile
end
end

Resources