I have used friendly_id and globalize gem. So I can generate routes as;
/search/france/weekly/toyota-95
Here is my routes;
namespace :search do
resources :car_countries, path: '', only: [] do
resources :rental_types, path: '', only: [] do
resources :car_types, path: '', only: [:show] do
end
end
end
end
But the thing is now I would like to also get city either;
/search/nice/weekly/toyota-95
or
/search/france/nice/weekly/toyota-95
The problem is I want to have both with city name and without city name (only country). They should go to same controller which is at the end car_types.
So if I add car_cities to routes, I get error when there is no city but only country.
namespace :search do
resources :car_countries, path: '', only: [] do
resources :car_cities, path: '', only: [] do
resources :rental_types, path: '', only: [] do
resources :car_types, path: '', only: [:show] do
end
end
end
end
resources :car_countries, path: '', only: [] do
resources :rental_types, path: '', only: [] do
resources :car_types, path: '', only: [:show] do
end
end
end
end
How can I do that?
As Gerry says, take a look at route globbing http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments. I'd suggest to send everything to a single controller action and either do stuff there or delegate it to a search model/service object (depends on your taste).
Example:
# in config/routes.rb
get 'search/*q' => 'searches#show'
# in app/controllers/searches_controller.rb
class SearchesController < ApplicationController
def search
# This should work for your simple use case but it will become pretty confusing if you add more filters.
search_params = params[:search].split('/')
if search_params.count == 4
country, city, rental_type, car_type = search_params
else
country, rental_type, car_type = search_params
end
# Do whatever with these variables, e.g. Car.for_country(country)...
end
end
A more stable solution would be to make use of the fact that rental types are probably a closed set (daily, weekly, ...) and use segment constraints for this part in the routes:
# config/routes.rb
scope to: 'searches#show', constraints: { rental_type: /(daily|weekly|monthly)/ } do
get '/search/:country/:rental_type/:car_type'
get '/search/:country/:city/:rental_type/:car_type'
end
This should differentiate the two URLs based on the fact that :city can never match the rental type constraint.
Yet another option would be to use a full blown constraints object (http://guides.rubyonrails.org/routing.html#advanced-constraints):
# config/routes.rb
class SearchConstraint
def initialize
# assuming that all your objects have the friendly_id on the name field
#country_names = %w[austria germany]
#rental_type_names = %w[daily weekly monthly]
#car_type_names = %w[toyota-prius vw-golf]
#city_names = %w[innsbruck munich berlin]
end
def matches?(request)
checks = []
# only check for parts if they're actually there
checks << #country_names.include?(request.parameters[:country]) if request.parameters[:country].present?
checks << #rental_type_names.include?(request.parameters[:rental_type]) if request.parameters[:rental_type].present?
checks << #car_type_names.include?(request.parameters[:car_type]) if request.parameters[:car_type].present?
checks << #city_names.include?(request.parameters[:city]) if request.parameters[:city].present?
checks.all? # or, if you want it more explicit: checks.all? { |result| result == true }
end
end
scope to: 'searches#show', constraints: SearchConstraint.new do
get '/search/:country/:rental_type/:car_type'
get '/search/:country/:city/:rental_type/:car_type'
end
Note that this last approach is probably the cleanest and least hacky approach (and it's quite easy to test) but it also comes at the cost if involving the database in every request to these particular URLs and the URLs fail hard if there's an issue with the database connection.
Hope that helps.
Related
Apologies for the basic question, but I'm trying to create an endpoint so I can a TranslationApi in my backend via my VueJs frontend via Fetch, so I need to make an endpoint I can insert. I'm attempting to create a route to make that happen, however when I run bin/rails routes | grep CcApiController I receive the following error:
ArgumentError: 'CcApiController' is not a supported controller name. This can lead to potential routing problems. See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use
I've read the documentation linked, but I'm not managing to fix this, can someone explain where I'm going wrong here? I'll link the files I've changed below:
cc_apis_controller.rb
module Panel
class CcApisController < CcenterBaseController
def index
run Ccenter::Adapters::Zendesk::TranslationApi.call(2116449)
end
end
end
panel_routes.rb
def draw_api_routes
resources: CcApiController
end
routes.rb
Rails.application.routes.draw do
resources :CcApiController, only: [:index]
end
API method I need to create Route for:
def make_request
response = Faraday.post('https://api.deepl.com/v2/translate', auth_key: '', text: #final_ticket, target_lang: 'DE', source_lang: 'EN')
if response.status == 200
body = response.body
message_element = body.split('"')[-2]
return message_element
else
raise InvalidResponseError unless response.success?
end
end
The answer to that is pretty simple.
Route names are snake_case, and match the controller's file name just omit the _controller suffix. In your case it is
Rails.application.routes.draw do
namespace :panel do
resources :cc_apis, only: %i[index]
end
end
For more info check this documentation and this article.
I'm using Caracal Gem to export a Docx file, the gem is generating the document normally. But when I try to print the contract data I get the error. It's probably some routing problem, I'm stuck in this two weeks and I'm almost giving up.
The error is Action Record Not Found Couldn't fid contrato with id=
Here is my code I highlighted the parts in the code.
Routes.rb
Rails.application.routes.draw do
get 'grayscale/index'
get 'contratos/page' HERE IS THE ROUTING
devise_for :users, path: '', path_names: {sign_in: 'login', sign_out: 'logout', sign_up: 'registrar'}
resources :contratos
root 'contratos#index'
get '/contrato_export' => 'contratos#export'
resources :contratos do
member do
# /contratos/:id/export
get 'export'
end
end
end
contratos_controller.rb
before_action :authenticate_user!
before_action :set_contrato, only: [:show, :edit, :update, :destroy, :export]
access all: [:show, :index], user: {except: [:destroy, :new, :create, :update, :edit]}, site_admin: :all
require './lib/generate_pdf'
def page
contrato = Contrato.find(params[:id])<---HERE IS THE PROBLEM!!!!!
Caracal::Document.save(Rails.root.join("public", "example.docx")) do |docx|
docx.style do
id 'Body'
name 'body'
font 'Times New Roman'
size 24
end
docx.h2 'Contrato'
docx.p do
style 'Body'
text 'Lorem ipsun dolor sit amet'
text contrato.day <--- HERE IS THE DATA THAT I WANT TO PRINT IN THE DOCX. (IN THE CONTRATO SHOW is #contrato.day, I WANT THAT DATE TO BE PRINTED.
end
end
path = File.join(Rails.root, "public")
send_file(File.join(path, "example.docx"))
end
You have a missing params
get 'contratos/:id/page', to: 'contratos#page'
#or
resources :contratos do
member do
get 'export' # /contratos/:id/export
get 'page' # add this!
end
end
That tutorial is just utter garbage. You can do the exact same thing (or at least what I think your trying to do) RESTfully without adding any additional routes just by using ActionController::MimeResponds:
class ContratosController < ApplicationController
# GET /contratos/1
# GET /contratos/1.pdf
# GET /contratos/1.docx
def show
respond_to do |format|
format.html {}
format.pdf { send_file Contratos::PdfConverter.call(#contrato) }
format.docx { send_file Contratos::XMLConverter.call(#contrato) }
end
end
end
The key here is keep your controller skinny. Controllers are notoriously hard to test.
We haven't actually declared Contratos::PdfConverter yet but this is one place where you could just use the service object pattern:
# app/services/base_service.rb
class BaseService
def self.call(*args, **kwargs, &block)
new(*args, kwargs, &block)
end
end
# app/services/contratos/docx_converter.rb
module Contratos
class DocxConverter < BaseService
# #return [Pathname]
def call(contrato)
path = Rails.root.join("public", "example.docx")
Caracal::Document.save(path) do |docx|
docx.style do
id 'Body'
name 'body'
font 'Times New Roman'
size 24
end
docx.h2 'Contrato'
docx.p do
style 'Body'
text 'Lorem ipsun dolor sit amet'
text contrato.day # ...
end
end
path
end
end
end
# app/services/contratos/pdf_converter.rb
module Contratos
class PdfConverter < BaseService
def call(contrato)
# generate PDF here
end
end
end
This lets you test the conversion separately and avoids turning your controller into a flaming garbage pile.
I'm developing an application which backend is being written in rails 5 api (beta version).
My API will have some versions, and I'm using this approach to address versioning:
https://github.com/iamvery/rails-api-example/blob/master/config/routes.rb
Rails.application.routes.draw do
def api_version(version, &routes)
api_constraint = ApiConstraint.new(version: version)
scope(module: "v#{version}", constraints: api_constraint, &routes)
end
api_version(1) do
resources :articles, only: :index
end
api_version(2) do
resources :articles, only: :index
end
end
The thing is when I don't specify a version, it shows me an (obviuos) error (ActionController::RoutingError: No route matches [GET] \...).
But I'd like to route using latest api version instead throwing an error.
Your routes.rb file
Rails.application.routes.draw do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
# Then for a new version create a new scope
end
end
Create a new api_constraints.rb file in the app/lib directory
class ApiConstraints
def initialize(options)
#version = options[:version]
#default = options[:default]
end
def matches?(req)
#default || req.headers['Accept'].include?("application/vnd.marketplace.v#{#version}")
end
end
I would add a root route, and use a simple redirect, like this:
root to: redirect('/api/v2')
I believe this could be done dynamically, by a little more change, something like this:
#versions = []
def api_version(version)
#versions << versions
# The rest of your code..
end
root to: redirect("/v#{#versions.max}")
I hope this helps.
How to draw a route with an IPv4 address as param. Just like this:
http://localhost:3000/ip/192.168.2.2
This route should only be used if request.params[:ipaddress] is an IPv4 address, but all I get is: No route matches [GET] "/ip/192.168.2.2"
First try: constraints: { ipaddress: /^regexp$/ }
Defining the constraint expression inline in .config/routes.rb like this:
resources :ipaddresses, path: :ip, param: :ipaddress, constraints: {
ipaddress: /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ }, only: :show
leads to an error and the server does not start:
/home/mschmidt/.rvm/gems/ruby-2.2.3/gems/actionpack-4.2.4/lib/action_dispatch/routing/mapper.rb:207:in
`verify_regexp_requirement': Regexp anchor characters are not allowed in routing requirements
Second try: without anchor characters
If I remove the anchor characters the server does start, but the constraint does not express what is wanted. The string can contain one or more IPv4 addresses, now.
Third try: move it to a function
So, I put the expression into a class with a method called matches?. This works perfect if the param does not have any dot. Unfortunately all these IPs have some dots and I'm back where I started:
No route matches [GET] "/ip/192.168.2.2"
/config/routes.rb:
Rails.application.routes.draw do
class IPv4Constraint
def matches?(request)
request.params[:ipaddress] =~ /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
end
end
resources :ipaddresses, path: :ip, param: :ipaddress, constraints: IPv4Constraint.new, only: :show
end
Because of the dots I have to define a constraint in routes.rb anyways and I think it wouldn't be very DRY to put more code for this into the controller. What is the best way to do this? Is there even a way to do this with a single expression?
I'm going to use routing-filter for now.
After adding the gem to the Gemfile:
gem 'routing-filter'
I added a new filter:
# The IPv4Address filter extracts segments matching /ip/:IPv4Address from the
# end of the recognized url and exposes the page parameter as
# params[:ipv4address]. When a url is generated the filter adds the address to
# the url accordingly if the page parameter is passed to the url helper.
#
# incoming url: /ip/192.168.2.2
# filtered url: /ip
# generated url: /ip/192.168.2.2
#
# You can install the filter like this:
#
# # in config/routes.rb
# Rails.application.routes.draw do
# filter :ipv4address
# end
module RoutingFilter
class Ipv4address < Filter
IPV4ADDRESS_PATTERN = %r(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)
def around_recognize(path, env, &block)
pattern = pattern_builder(path)
ipv4address = extract_segment!(pattern, path)
yield.tap do |params|
params[:ipv4address] = ipv4address if ipv4address
end
end
def around_generate(params, &block)
ipv4address = params.delete(:ipv4address)
yield.tap do |result|
append_segment!(result, "ip/#{ipv4address}") if append_ipv4address?(ipv4address)
end
end
private
def pattern_builder(path)
ipv4address = extract_ipv4address(path)
build = "/(#{ipv4address.gsub('.','\.')})/?$"
/#{build}/
end
def extract_ipv4address(path)
path.match(IPV4ADDRESS_PATTERN).to_s
end
def append_ipv4address?(ipv4address)
ipv4address && !ipv4address.blank?
end
end
end
called it in routes.rb and changed the action from show to index
filter :ipv4address
resources :ipaddresses, path: :ip, only: :index
The IP is now available in my controller as params[:ipv4address]
I have URLs like this
arizona/AZ12
colorado/CO470
I added the AZ and CO because friendly id wanted unique ids. Arizona and Colorado could have a unit 12.
I'd like to have URLs like
arizona/unit12
colorado/unit470
Seems like you could write something that removes the first two characters and replaces them. Would that be in the routes or controller?
My routes
resources :states, :except => [:index ], :path => '/' do
resources :units, :except => [:index ], :path => '/'
end
My controller
def show
#units = Unit.all
#states = State.with_units.group('states.id')
#state = State.all
#unit = Unit.friendly.find(params[:id])
end
Implement to_param method on your model. Rails will call to_param to convert the object to a slug for the URL. If your model does not define this method then it will use the implementation in ActiveRecord::Base which just returns the id.
class SomeModel
def to_param
"unit#{id}"
end
end
You can refer https://gist.github.com/agnellvj/1209733 for example