I am trying to render a specific view path based on the version of my design (stored as a db column).
The idea is if the version of my design is 1, I will render
products/v1/show.html.erb
if 2 :
products/v2/show.html.erb
To achieve this I have tried in ProductsController :
class ProductsController < ApplicationController
before_action :set_view_paths
def set_view_paths
prepend_view_path Rails.root.join('app', 'views', 'products', "v#{current_tenant.ui_version}")
end
end
and nesting my show template in v1/show.html.erb.
However now that I've moved the show template, I get ProductsController#show is missing a template for request formats: text/html
Am I missing something about prepend_view_path method ?
Clarification about preprend_view_path
https://api.rubyonrails.org/classes/ActionView/ViewPaths/ClassMethods.html
Before to had something to it, you can look at the current array.
It should contain app/views.
Then your ProductsController#show will look within it for products/show
I guess you get a bit confused here.
If you use:
prepend_view_path Rails.root.join('app', 'views', "v#{current_tenant.ui_version}")
Then your product folder will look like this:
app/views
v1/products/show
v2/products/show
prepend_view_path has a more global impact.
Live split versions in product folder
If you want:
app/views/products
v1/show
v2/show
You can override the render method:
class ProductsController < ApplicationController
# actions ..
private
def render(*args)
change_some_args_here = "/#{design_version_name}/#{params[:action]}"
super(*args)
end
def design_version_name
# implement me
end
end
EDIT: now this concept is native with variants option
https://guides.rubyonrails.org/layouts_and_rendering.html#the-variants-option
Changing controller view path
There is also the controller_path class method
def self.controller_path
"products/v1"
end
However as you can see, v1 is static here.
Class-method means you don't know about the "live" context ...
Conclusion
I guess the second method is what match your need.
EDIT: the variants option looks like the direct answer to your question.
Related
I would like to ask if i should keep the empty methods in my controller (Its a question about code style):
before_action :set_project, only: [:show,:new]
def show
end
def new
end
Should i keep it like this or simpy remove show and new action
class ProjectController < ApplicationController
before_action :set_project
def index
#indexaction
end
def create
#createaction
end
is it more Railish way? Rails Styleguide doesnt indicate any sollution to it, only that:
def show
end
is better than
def show; end
If you're not defining any data in those methods, you can remove them.
Rendering By Default:
Rails automatically infers the view from action names in routes.
If you're not defining data (only to be done in the controller), you'll can rely on the view being loaded without the action being present:
You've heard that Rails promotes "convention over configuration". Default rendering is an excellent example of this. By default, controllers in Rails automatically render views with names that correspond to valid routes. For example, if you have this code in your BooksController class:
class BooksController < ApplicationController
end
And the following in your routes file:
resources :books
And you have a view file app/views/books/index.html.erb:
<h1>Books are coming soon!</h1>
Rails will automatically render app/views/books/index.html.erb when you navigate to /books and you will see "Books are coming soon!" on your screen.
Good ref: Rails doesn't need index method in controller defined?
If you want to use those methods in future, keep those methods, else remove them. There will not be any problem even if they are kept.
Also remove the routes to those methods if they are created and you dont want to use them. Code should be as simple as it can.
If it's truly about style first keep your comments above, so that if you ever run rdoc or generate docs, they take whatever comments come before the def show and use that to build out the paragraph describing what this method "does".
Example
class ProjectController < ApplicationController
before_action :set_project
#indexaction is awesome and will be used to do stuff like look at all the projects
def index
end
# will hopefully create your project
def create
end
Yeah but don't keep it if it isn't used...
If routes aren't used, don't have them lying around. By default you get things like blah.com/projects/1.json and if you haven't locked this down (or even if you have and a user is inside the app) they could easily get the json result, assuming you left everything there for later. Let alone if you have a before_filter that does 'stuff' to projects on load`.
commit it once into git and then delete it and commit again. If you ever need to reinstate it, at least github/bitbucket history or git history if you are sadistic, will have it to copy and paste.
Let say we have a code:
Model:
class Dog < ActiveRecord::Base
def self.make_noise
puts 'bow-wow'
end
end
Controller:
class DogsController < ApplicationController
def index
Dog.make_noise
end
end
This will work, but I would rather like to write the controller index method code like: AssociatedModel.make_noise or Model.make_noise
Is it possible in Rails to call associated model method without using its class name in code?
This would be useful if I would like to use inheritance and make let say PetsController which will be the base for all pets (or a PetNoise Concern included for every applicable controller) and declare there index method.
I'm not sure if I explained this well enough.
OK. The one way (which i don't like) is to write PetsController method like this:
def index
params[:controller].classify.constantize.make_noise
end
This way if I inherit PetsController from DogsController it will still work without defining separate index inside DogsController. But maybe there are other more neat solutions.
As I also explained in this answer, you can determine the model using params[:controller]. Like this:
params[:controller] # => "dogs"
params[:controller].classify # => "Dog"
Therefore you can write your index action "generically" like this:
def index
model_class = params[:controller].classify.constantize
model_class.make_noise
end
I've tried Facebook's Open Graph protocol in adding meta data on Rails pages. What I want to do now is to make my code not duplicated or D.R.Y.---instead of putting one meta-data header for each controller page I have, I'd like to create a base class called "MyMetaBuilder" which will be inherited by the sub-pages, but don't know where and how to start coding it...
Someone suggested that meta data property values must be dynamically generated depending on the context. For example, PlayMetaBuilder, CookMetaBuilder and so on...
Also, when unit testing the controller action, how do I verify for its existence?
Thanks a lot.
One thing is defining the tags, another is rendering them. I would do the following:
write a controller mixin (something like acts_as_metatagable) where I would define specific fields for each controller (and populate the remaining with defaults). These would be assigned to a class (or instance) variable and in this way be made accessible in the rendering step).
write an helper function which would take all my tags and turn them into html. This helper function would then be called in the layout and be rendered in the head of the document.
so, it would look a bit like this:
# homepage_controller.rb
class HomepageController < ActionController::Base
# option 1.2: include it directly here with the line below
# include ActsAsMetatagable
acts_as_metatagable :title => "Title", :url => homepage_url
end
# lib/acts_as_metatagable.rb
module ActsAsMetatagable
module MetatagableMethods
#option 2.2: insert og_tags method here and declare it as helper method
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
def self.included(base)
base.helper_method :og_tags
end
end
def acts_as_metagabable(*args)
include MetatagableMethods
# insert dirty work here
end
end
# option 1.1: include it in an initializer
# initializers/acts_as_metatagable.rb
ActiveController::Base.send :include, ActsAsMetatagable
# option 2.1: insert og_metatags helper method in an helper
module ApplicationHelper
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
end
What I did for Scoutzie, was put all metadata into a head partial, with if/else cases as such:
%meta{:type => 'Author', :content => "Kirill Zubovsky"}
%meta{'property' => "og:site_name", :content=>"Scoutzie"}
-if #designer
...
-elsif #design
...
-else
...
This way, depending on the variables that load, I know which page it is, and thereby know which metadata to include. This might not be an elegant solution, but it works and it's really simple.
I have several of my partials in a folder named partials, and I render them into my view using render 'partials/name_of_my_partial' and that's okay.
Anyhow, is it possible to set up things in a way than I could just use render 'name_of_my_partial' and rails automatically check this partials folder?
Right now, I'm having a Missing partial error.
In rails 3.0 this a bit of a challenge, but looking into it I found that in rails 3.1, they've changed how path lookup works, making it much simpler. (I don't know what exact version they changed it though, it may have been much earlier).
In 3.1, this is relatively simple because they've introduced a way to send multiple prefixes to the path lookup. They are retrieved via the instance method _prefixes.
It's easy to tack on an arbitrary prefix to this, for all controllers, by simply overriding it in the base controller (or in a module you include in your base controller, whichever).
So in 3.1.x (where lookup uses multiple prefixes):
class ApplicationController
...
protected
def _prefixes
#_prefixes_with_partials ||= super | %w(partials)
end
end
Prior to this change, a single prefix was used for lookup, which made this a lot more complicated. This may have not been the best way, but I solved this problem in the past by rescuing from missing template errors with an attempt to look up the same path with my "fallback" prefix.
In 3.0.x (where lookup uses a single path prefix)
# in an initializer
module YourAppPaths
extend ActiveSupport::Concern
included do
# override the actionview base class method to wrap its paths with your
# custom pathset class
def self.process_view_paths(value)
value.is_a?(::YourAppPaths::PathSet) ?
value.dup : ::YourAppPaths::PathSet.new(Array.wrap(value))
end
end
class PathSet < ::ActionView::PathSet
# our simple subclass of pathset simply rescues and attempts to
# find the same path under "partials", throwing out the original prefix
def find(path, prefix, *args)
super
rescue ::ActionView::MissingTemplate
super(path, "partials", *args)
end
end
end
ActionView::Base.end(:include, YourAppPaths)
I haven't been able to find other resources than this SO question on this topic, so I'm posting about my efforts here.
In Rails 3.2+ (Also tested in 4.2), one can access and modify lookup_context.prefixes from within a controller action. So, to modify lookup of template & partial prefixes to include another path you can do this:
class MyObjectsController < ApplicationController
def show
# WARNING: Keep reeding for issues with this approach!
unless lookup_context.prefixes.first == "partials"
lookup_context.prefixes.prepend "partials"
end
end
end
This way, if there is a show.html.erb template in the app/views/partials/ folder then it will be rendered. And the same goes for any partials referenced in show.html.erb.
What's going on here?
The method lookup_context returns an object of type ActionView::LookupContext, which is the object responsible to hold all information required to lookup templates in ActionView. Of note, it also gives you access to lookup_context.view_paths, which is not what is being asked for in this question but sounds like it should be.
WARNING
Modification of the lookup_context.prefixes array is cached for all future requests. Therefore, in order to use it problem-free, it's best to make sure we also remove any prefixes we add.
So, is there an easy way to do this?
Sure. For my own projects, I've created a module which I can include in any controller that needs this ability (or just include it in ApplicationController). Here's my code:
# Helper methods for adding ActionView::LookupContext prefixes on including
# controllers. The lookup_context.prefixes collection is persisted on cached
# controllers, so these helpers take care to remove the passed-in prefixes
# after calling the block.
#
# #example Expected Usage:
# around_action only: :show do |_, block|
# prepend_lookup_context_prefixes("my_optional_name_space/my_objects", &block)
# end
#
# around_action only: %i[index edit update] do |_, block|
# append_penultimate_lookup_context_prefixes("my_optional_name_space/my_objects", &block)
# end
module PrefixesHelper
# Prepends the passed in prefixes to the current `lookup_context.prefixes`
# array, calls the block, then removes the prefixes.
#
# #param [Array<String>] prefixes
def prepend_lookup_context_prefixes(*prefixes, &block)
lookup_context.prefixes.prepend(*prefixes)
block.call
remove_lookup_context_prefixes(*prefixes, index: 0)
end
# Sets the penultimate (2nd-to-last) prefixes in the current
# `lookup_context.prefixes` array, calls the block, then removes the prefixes.
#
# #param [Array<String>] prefixes
def append_penultimate_lookup_context_prefixes(*prefixes, &block)
lookup_context.prefixes.insert(-2, *prefixes)
block.call
remove_lookup_context_prefixes(*prefixes.reverse, index: -2)
end
# Removes the passed in prefixes from the current `lookup_context.prefixes`
# array. If index is passed in, then will only remove prefixes found at the
# specified index in the array.
#
# #param [Array<String>] prefixes
# #param [Integer] index
def remove_lookup_context_prefixes(*prefixes, index: nil)
prefixes.each do |prefix|
if index
if lookup_context.prefixes[index] == prefix
lookup_context.prefixes.delete_at(index)
end
else
lookup_context.prefixes.delete(prefix)
end
end
end
end
As mentioned in the comments in this module, the expected usage for this module is to call the methods contained within via an around_filter call in your controller. This way, the module will take care of removing any prefixes it adds after the controller action has yielded.
For example:
around_filter only: :show do |_, block|
prepend_lookup_context_prefixes("my_optional_name_space/my_objects", &block)
end
Or:
around_filter only: %i[index edit update] do |_, block|
append_penultimate_lookup_context_prefixes("my_optional_name_space/my_objects", &block)
end
I've also included the PrefixesHelper module posted here into a gem that I use to add a few nice extensions such as these to my Rails apps. In case you'd like to use it too, see here: https://github.com/pdobb/core_extensions
Note: I've abandoned this approach due to some weird behavior and incompatibility with the multi_fetch_fragments gem
If you'd like to prefix your partials for an STI collection, I've come up with the following monkey patch (for Rails 3.2, Ruby 2.2+).
config/initializers/partial_renderer_prefix.rb
module ActionView
class PartialRenderer
def merge_prefix_into_object_path(prefix, object_path)
# Begin monkey patch
if #options.key?(:prefix)
prefixes = [#options[:prefix]]
return (prefixes << object_path).join("/")
end
# End monkey patch
if prefix.include?(?/) && object_path.include?(?/)
prefixes = []
prefix_array = File.dirname(prefix).split('/')
object_path_array = object_path.split('/')[0..-3] # skip model dir & partial
prefix_array.each_with_index do |dir, index|
break if dir == object_path_array[index]
prefixes << dir
end
(prefixes << object_path).join("/")
else
object_path
end
end
end
end
In a view, use it like this:
<%= render #order_items, prefix: 'orders/timeline' %>
Where you have app/views/orders/timeline/product_order_items/_product_order_item.html.erb and app/views/orders/timeline/subscription_order_items/_subscription_order_item.html.erb partials available for each of your STI models.
I am writing my first plugin, and in that plugin, I need to run a method for some controller/action pairs. For this plugin the configuration yml looks like this -
track1:
start_action: "home", "index"
end_action: "user", "create"
So, in my plugin I will first read above yml file. After that I want to run an action say - first_function as before_filter to home-controller index-action and I would be running second_function as after_filter for user-controller create-action.
But I couldn't figure out how can I write filters for this, which will be declared in plugin and will run for actions specified by user in above yml files.
Please help !
I see two options to reach your goal.
First approach: declare both the filters inside the ApplicationController and inside the filter check whether the controller_name and action_name match any in your yaml-configuration. If they match, execute it, if not ignore.
In code that would like
class ApplicationController
before_filter :start_action_with_check
after_filter :end_action_with_check
def start_action_with_check
c_name, a_name = CONFIG['track1']['start_action'].split(', ')
if c_name == controller_name && a_name == action_name
do_start_action
end
end
...
I hope you get the idea.
Second approach: a clean way to define before_filter is to define them in a module. Normally you would use the self.included method to define the before_filter, but of course, you can define them conditionally. For example:
class HomeController < ApplicationController
include StartOrEndAction
...
end
and in lib/start_or_end_action.rb you write
module StartOrEndAction
def self.included(base)
# e.g. for the start-action
c_name, a_name CONFIG['track1']['start_action'].split(', ')
if c_name == controller_name && a_name == action_name
base.before_filter :do_start_action
end
# and do the same for the after_filter
end
def do_start_action
...
end
def do_end_action
...
end
end
The advantage of the second solution is that the before_filter and after_filter are only defined when needed. The disadvantage is that you will have to include the module into each controller where you could possible configure the before-filter to take place.
The first has the advantage that any controller is covered, and you get a little overhead checking the before- and after filter.
Hope this helps.