add dynamic route from rails database model? - ruby-on-rails

How can I add dynamic route from database, using model activeRecord :
I try : (using Rails 5)
Rails.application.routes.draw do
router = self
Var.find_by(name: 'pages').value.split(',').each do |page|
router.get "/#{page}", to: 'application#page', as: page
end
end
but I have an error when I try start rails server :
`rescue in mysql2_connection': Unknown database 'dale' (ActiveRecord::NoDatabaseError)

You can move the code that access the db to initialize block.
Rails.application.routes.draw do
router = self
Rails.application.config.after_initialize do
Var.find_by(name: 'pages').value.split(',').each do |page|
router.get "/#{page}", to: 'application#page', as: page
end
end
end
There is no guarantee that your initializers will run after all the gem initializers, so any initialization code that depends on a given gem having been initialized should go into a config.after_initialize block.
Rails has 5 initialization events which can be hooked into (listed in the order that they are run): Further details in Rails documentation initialization events

The issue you're encountering is because Var.find_by(name: 'pages') is trying to run ActiveRecord query, but the Rails.application.routes.draw block is executed when the application starts and before the database connection is established.
To solve this, you can move the logic for defining the dynamic routes to a controller action, which will run after the database connection is established. You can then use redirect_to in the action to redirect to the appropriate route:
# in your controller
def pages
page = params[:page]
redirect_to "/#{page}"
end
# in your routes file
Rails.application.routes.draw do
get 'pages', to: 'application#pages'
Var.find_by(name: 'pages').value.split(',').each do |page|
get "/#{page}", to: 'application#page', as: page
end
end
By this way, when a request is made to the /pages URL, the pages action will run and redirect to the appropriate route based on the page parameter.
I hope this helps.

Related

Devise authenticate_admin_user! does not work after upgrading to rails 5

I have an old, rather big Rails app I need to upgrade to a current version. It is currently running on Rails 4.2.11. I've managed to upgrade all my gems now, so it runs Rails version 5.0.7. And I am in a state where the app starts again and mostly works. While doing so, I've upgraded the devise gem from version 3.4.0 to 4.0.0, but I've also tried 4.7.3. It does not make a difference to my problem.
The only thing which does not work correctly is authentication. I can load the login screen and login with a user. The login is successful, but then I get redirected back to the main application page, instead of the protected resource.
From what I could found out, the Devise session is not persisted in the session, but I don't understand why it does not work. I don't get any error in the log. The log displays the initial 401 error when I request the protected resource, and we are redirected to the login form (as expected). After a successful login (I see the sign_in_count increase in the database), a redirect to the home page happens, instead of the protected resource.
I've added the following code into the index method of the main page controller (to which I get redirected):
class MainController < ApplicationController
def index
puts "Current Admin User: #{current_admin_user} nil: #{current_admin_user.nil?} signedIn: #{admin_user_signed_in?}"
# rest of the code omitted for simplicity
end
end
The output is as follows:
web_1 | [pid: 1] [c48b7285-3f9e-4cb7-94ba-64b6c9d9bd0e] Processing by MainController#index as HTML
web_1 | Current User: is nil: true signed_in: false
The (simplified) routes.rb file looks like this:
root 'main#index'
devise_for :admin_users
namespace :admin do
constraints(CheckIp.new) do
devise_scope :admin_user do # a
root to: '/admin/main#index' # b
resources :main_admin, path: :main do
... # contains sub resources
end
end
end
end
I've added the lines a and b after the upgrade in the hope it fixes my issues, but I could not see any difference. My understanding is that the devise 4 should redirect to the root (line b) inside my scope, but this is not happening. I also tried to move the line a before the constraints check and again before the admin namespace. The results are the same in all cases.
Routes have priority in the order they are defined.
Since root 'main#index' was defined at the top of the file Rails will already match the request for / before it gets to your second route with the constraint.
All you have to do is move the default route below the constraint:
devise_for :admin_users
namespace :admin do
constraints(CheckIp.new) do
devise_scope :admin_user do # a
root to: '/admin/main#index' # b
resources :main_admin, path: :main do
... # contains sub resources
end
end
end
end
root 'main#index'
That way it "falls through" if the constraint or devise_scope does not produce a matching route.
I've finally found the reason for my issues. I've made some modification to the middleware stack for log tagging like this:
Rails.configuration.middleware.delete(ActionDispatch::Cookies)
Rails.configuration.middleware.delete(ActionDispatch::Session::CookieStore)
Rails.configuration.middleware.insert_before(Rails::Rack::Logger, ActionDispatch::Session::CookieStore)
Rails.configuration.middleware.insert_before(ActionDispatch::Session::CookieStore, ActionDispatch::Cookies)
This does not longer work. So for the time being I remove the log tagging, as authentication is more important.

How to verify controller actions are defined for all routes in a rails application?

Is there a way to verify that all controller actions, as defined in config/routes.rb and exposed by rake routes, actually correspond to an existing controller action?
For example, suppose we have the following routes file:
Application.routes.draw do
resources :foobar
end
And the following controller:
class FoobarsController < ApplicationController
def index
# ...
end
def show
# ...
end
end
I'd like to have some way of auto-detecting that the create, new, edit, update and destroy actions (as implicitly defined by the routes) are not mapped to a valid controller action - so that I can fix the routes.rb file:
Application.routes.draw do
resources :foobar, only: [:index, :show]
end
An "integrity check" of the routes, if you will.
Such a check wouldn't necessarily need to be perfect; I could easily verify any false positives manually. (Although a "perfect" check would be ideal, as it could be included in the test suite!)
My motivation here is to prevent AbstractController::ActionNotFound exceptions from being raised by dodgy API requests, as additional routes were inadvertently defined (in a large application).
I got curious, and the following is my attempt. It's still not accurate because it does not yet match proper format. Also, some routes have constraints; my code doesn't yet consider.
rails console:
todo_skipped_routes = []
valid_routes = []
invalid_routes = []
Rails.application.routes.routes.each do |route|
controller_route_name = route.defaults[:controller]
action_route_name = route.defaults[:action]
if controller_route_name.blank? || action_route_name.blank?
todo_skipped_routes << route
next
end
# TODO: maybe Rails already has a "proper" way / method to constantize this
# copied over #max answer, because I forgot to consider namespacing
controller_class = "#{controller_route_name.sub('\/', '::')}_controller".camelcase.safe_constantize
is_route_valid = !controller_class.nil? && controller_class.instance_methods(false).include?(action_route_name.to_sym)
# TODO: check also if "format" matches / gonna be "responded to" properly by the controller-action
# check also "lambda" constraints, and `request.SOMEMETHOD` constraints (i.e. `subdomain`, `remote_ip`, `host`, ...)
if is_route_valid
valid_routes << route
else
invalid_routes << route
end
end
puts valid_routes
puts invalid_routes
# puts "friendlier" version
pp invalid_routes.map(&:defaults)
# => [
# {:controller=>"sessions", :action=>"somenonexistingaction"},
# {:controller=>"posts", :action=>"criate"},
# {:controller=>"yoosers", :action=>"create"},
# ]
I am also interested to know other answers, or if there is a proper way to do this. Also, if anyone knows an improvement over my code, please let me know. Thanks :)
This builds on Jay-Ar Polidario's answer:
require 'test_helper'
class RoutesTest < ActionDispatch::IntegrationTest
Rails.application.routes.routes.each do |route|
controller, action = route.defaults.slice(:controller, :action).values
# Some routes may have the controller assigned as a dynamic segment
# We need to skip them since we can't really test them in this way
next if controller.nil?
# Skip the built in Rails 5 active_storage routes
next if 'active_storage' == controller.split('/').first
# Naive attempt to resolve the controller constant from the name
# Replacing / with :: is for namespaces
ctrl_name = "#{controller.sub('\/', '::')}_controller".camelcase
ctrl = ctrl_name.safe_constantize
# tagging SecureRandom.uuid on the end is a hack to ensure that each
# test name is unique
test "#{ctrl_name} controller exists - #{SecureRandom.uuid}" do
assert ctrl, "Controller #{ctrl_name} is not defined for #{route.name}"
end
test "#{controller} has the action #{action} - #{SecureRandom.uuid}" do
assert ctrl.respond_to?(action),
"#{ctrl_name} does not have the action '#{action}' - #{route.name}"
end if ctrl
end
end
However I would question if its actually usable for anything beyond the most trivial examples.
Huge credit to the other answers - please check them out below. But this is what I've ended up using on multiple projects over the past couple of years, and it's served me well. So I'm self-marking this as the accepted answer for visibility.
I placed the following in spec/routes/integrity_check_spec.rb:
require 'rails_helper'
RSpec.describe 'Integrity Check of Routes', order: :defined do # rubocop:disable RSpec/DescribeClass
Rails.application.routes.routes.sort_by { |r| r.defaults[:controller].to_s }.each do |route|
controller, action = route.defaults.slice(:controller, :action).values
# Some routes may have the controller assigned as a dynamic segment
# We need to skip them since we can't really test them in this way
next if controller.nil?
# Skip the built in Rails 5 active_storage routes
next if controller.split('/').first == 'active_storage'
# Skip built in Rails 6 action_mailbox routes
next if controller == 'rails/conductor/action_mailbox/inbound_emails'
ctrl_name = "#{controller.sub('\/', '::')}_controller".camelcase
ctrl_klass = ctrl_name.safe_constantize
it "#{ctrl_name} is defined and has corresponding action: #{action}, for #{route.name || '(no route name)'}" do
expect(ctrl_klass).to be_present
expect(ctrl_klass.new).to respond_to(action)
end
end
end
Caveats:
This is only a basic check for "does the controller action exist?". It does not consider parameters, format, subdomains, or any other route constraints. However, in my experience, that's good enough for the vast majority of scenarios.
This test only ensures that defined routes map to valid controller actions, not the converse. So it's still possible to have "dead code" in controllers, without a test failing. I haven't attempted to address that problem here.
It's possible for a route to have no controller action and still be valid!! This test can fail in such scenarios! As a workaround, you could - for example - define empty methods in the controller, instead of relying on the "magic" rails default behaviour. But the key takeaway here is: be careful when removing "dead" routes; you cannot immediately assume that a failing test here means the route is invalid.

Routing Error uninitialized constant controller

I'm am trying to learn Ruby on rails and I keep getting this error.
My controller is
class Clasa9Controller < ApplicationController
def multimi
end
def progresii
end
def functii
end
def vectori
end
def trigonometrie
end
def geometrie
end
end
clasa9.html.erb
<button class="btn"><%= link_to "", multimi_path %></button>
rails routes:
multimi GET /clasa_9/multimi(.:format) clasa_9#multimi
progresii GET /clasa_9/progresii(.:format) clasa_9#progresii
functii GET /clasa_9/functii(.:format) clasa_9#functii
vectori GET /clasa_9/vectori(.:format) clasa_9#vectori
trigonometrie GET /clasa_9/trigonometrie(.:format) clasa_9#trigonometrie
geometrie GET /clasa_9/geometrie(.:format) clasa_9#geometrie
and routes.rb
get 'clasa_9/multimi', to:"clasa_9#multimi", as:"multimi"
get 'clasa_9/progresii', to:"clasa_9#progresii", as:"progresii"
get 'clasa_9/functii', to:"clasa_9#functii", as:"functii"
get 'clasa_9/vectori', to:"clasa_9#vectori", as:"vectori"
get 'clasa_9/trigonometrie', to:"clasa_9#trigonometrie", as:"trigonometrie"
get 'clasa_9/geometrie', to:"clasa_9#geometrie", as:"geometrie"
devise_for :users
get 'pages/home'
get 'pages/clasa9'
get 'pages/clasa10'
get 'pages/clasa11'
get 'pages/clasa12'
get 'pages/about'
root 'pages#home'
and im am getting
Routing Error
uninitialized constant Clasa9Controller
I tried to solve this by looking up what is already posted here but I just can't solve it... I don't understand what I should change.
If your file is located inside the app/controllers folder, then it is probably a file name issue. Your file should have the name clasa9_controller.rb.
If not, then you should load the file by creating an initializer or by adding an autoload_path inside config/development.rb
Rails loads by default:
All subdirectories of app in the application and engines present at boot time. For example, app/controllers. They do not need to be the default ones, any custom directories like app/workers belong automatically to autoload_paths.
Any existing second level directories called app/*/concerns in the application and engines.
The directory test/mailers/previews.
Look it would be clasa9 but why that when you run this with the underscore method like this
Loading development environment (Rails 5.1.4)
2.3.4 :001 > "Clasa9Controller".underscore
=> "clasa9_controller"
it returns clasa9_controller that means your controller is clasa9 not clasa_9 and file name will be clasa9_controller.rb then your routes would be to: "clasa9#multimi" like this
get 'clasa_9/multimi', to: "clasa9#multimi", as: "multimi"
#or
#get 'clasa_9/multimi', to: "clasa9#multimi", as: :multimi # removed doublw quotes from multimi
...
Follow this it should work.

Route Constraints Not Working As Expected

I hoped route constraints would allow me to have admin.example.com/widgets/ and example.com/admin/widgets/ be effectively the same, with the URL helper admin_widgets_path pointing to the correct one based on the current subdomain. However, the route helper seems to only point to one or the other regardless of constraints.
Am I doing something wrong? Is this a bug? Is there a better way to solve this problem?
A rails app with an example of the problem has been published here, but the relevant details are below.
My config/routes.rb
class Subdomains
# any domain that starts with 'admin.'
def self.admin?
-> (request) { request.host =~ /^admin\./ }
end
# any other domain
def self.primary?
-> (request) { !admin?.(request) }
end
end
Rails.application.routes.draw do
constraints(Subdomains.primary?) do
namespace :admin do
resources :widgets
end
end
constraints(Subdomains.admin?) do
scope module: "admin", as: :admin do
resources :widgets
end
end
root to: "admin/widgets#index"
end
My app/views/admin/widgets/index.html.erb
<%= link_to "Widgets", admin_widgets_url %>
In this configuration, admin_widgets_url always returns /admin/widgets/ which isn't a valid route on admin.example.com so clicking the link results in a routing error on that subdomain. If the admin subdomain constraint block is put first in the routes, then the URL helper always returns /widgets/, breaking the link on a non-admin domain.
The routes otherwise work, but not sure how to get the URL helpers to point to the correct one.
Your expectations are wrong - Rails reads the entire routes definition as part of the setup phase. This is also when the route helpers are created. The constraint is not actually evaluated until later when the request is matched.
Rails splits this into distinct phases to allow forking.
Instead you may need to override the route helpers.

Routing Error with Post/Put requests (Passenger Headers)

I've run into a weird problem and after a bunch of research can't get any closer. I've got several forms that upload files via Carrierwave. When I upload the information, part of the route gets cut off (I think).
For example, I have a multi-part form submitting to:
https:/domain/programs/223/add_file as POST
but on submission I get the error
No route matches [POST] "/223/add_file"
even though what's in my address bar is the complete route. And if submit the complete route as a GET request it works fine. When I run rake routes the route shows up just fine.
Here is a subset of my route:
resources :programs do
match "add_file" => "programs#add_file"
If it matters, I'm running Rails 3.2.2 with Passenger on Apache. The problem only happens on this production server, never in development.
Any ideas? I'm stuck on this one as it effects multiple routes and I've tried defining a custom route just for that form with no luck.
Update:
When I remove multi-part => true or the file_field_tag from the form it fixes the problem. It's still an issue but seems to be less about routing than about the form with file uploads.
Create passenger_extension.rb in the lib folder with this code:
Passenger 3
module PhusionPassenger
module Utils
protected
NULL = "\0".freeze
def split_by_null_into_hash(data)
args = data.split(NULL, -1)
args.pop
headers_hash = Hash.new
args.each_slice(2).to_a.each do |pair|
headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
end
return headers_hash
end
end
end
Passenger 5
module PhusionPassenger
module Utils
# Utility functions that can potentially be accelerated by native_support functions.
module NativeSupportUtils
extend self
NULL = "\0".freeze
class ProcessTimes < Struct.new(:utime, :stime)
end
def split_by_null_into_hash(data)
args = data.split(NULL, -1)
args.pop
headers_hash = Hash.new
args.each_slice(2).to_a.each do |pair|
headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
end
return headers_hash
end
def process_times
times = Process.times
return ProcessTimes.new((times.utime * 1_000_000).to_i,
(times.stime * 1_000_000).to_i)
end
end
end # module Utils
end # module PhusionPassenger
And then in 'config/application.rb' do:
class Application < Rails::Application
...
config.autoload_paths += %W(#{config.root}/lib)
require 'passenger_extension'
end
And then restart a webserver.
NOTICE: I'm not sure that this doesn't break any other functionality so use it on your own risk and please let me know if you find any harm from this approach.
One issue here is you're not specifying whether the route is defined on the collection or a member. Which one of these is the correct route?
programs/:id/add_file
programs/add_file
You should construct your routes like this:
resources :programs do
post 'add_file', :on => :member
end
or
resources :programs do
member do
post 'add_file'
end
end
The above will take post requests at programs/:id/add_file and send them to ProgramsController.add_file with the params[:id] as the program id.
If you want this on the collection, you could do:
resources :programs do
post 'add_file', :on => :collection
end
or
resources :programs do
collection do
post 'add_file'
end
end
This would take post requests at programs/add_file and send them to ProgramsController.add_file, but no params[:id] would be set.
In general you should always specify whether routes are on the collection or member, and you should specify which verb a route should accept (ie use 'get' or 'post' etc. instead of 'match').
Try the above and see if that solves your problem, if not please let me know and I'll take another look.
I think you may need to add
:via => [:post]
to your route specification. It seems odd that it'd work on development and not on production, but as I understand rails routing, the matcher that you've added is only going to respond to get.
Try changing your match to
match "add_file" => "programs#add_file", :via => [:post]
Also, based on the answer just submitted by Andrew, you're probably better off using the member specifier to be explicit about the fact that the operation is happening on a particular Program with a particular id, and not the collection. It also should save some code in your add_file method which is probably working hard to get the id parameter from the url.

Resources