1
\$\begingroup\$

I'm trying to think of a better way to write these routes in my routes.rb file:

Rails.application.routes.draw do
  root 'home#index'

  s = /section1|section2|section3|section4/
  y = /\d{4}/
  m = /\d{1,2}/

  resources :articles, except: [:index, :show]

  # The slug will replace the default params[:id] of the following resources: 
  resources :articles, only: [:index, :show], path: '/:section/:year/:month', as: :my_articles, constraints: {:section => s, :year => y, month: m}

  get ':section/:year/:month', to: 'articles#by_month', as: :month, constraints: {section: s, year: y, month: m}
  get ':section/:year', to: 'articles#by_year', as: :year, constraints: {section: s, year: y}
  get ':section', to: 'articles#by_section', as: :section, constraints: {section: s}
end

This enables me to search articles by section, by year+section, by month+section+year. It also lets me add a nice slug to URLs.

One of the problems is that the constraints are common among all these routes. I'm wondering if there's a more clever way to go about writing this. Someone suggested I use concerns but I haven't figured out a way to do that.

Not a game changer, but I'm curious as to how I can write this properly.

\$\endgroup\$
0
\$\begingroup\$

Got it:

Rails.application.routes.draw do
  root 'home#index'

  resources :articles, except: [:index, :show]
  constraints section: /section1|section2|section3|section4/ do
    constraints year: /\d{4}/ do
      constraints month: /\d{1,2}/ do
        resources :articles, only: [:index, :show], path: '/:section/:year/:month', as: :my_articles
        get ':section/:year/:month', to: 'articles#by_month', as: :month
      end
      get ':section/:year', to: 'articles#by_year', as: :year
    end
    get ':section', to: 'articles#by_section', as: :section
  end

I feel this is much cleaner :)

\$\endgroup\$
0
\$\begingroup\$

I would not use routes constraints at all here and keep restful controller for simplicity and easy maintanance.

class ArticlesController < ApplicationController
  def index
    @articles = Article.by_section(params[:section])
                  .by_year(params[:year])
                  .by_month(params[:month])
  end
end

then defined scopes in model to allow missing params:

class Article < AR::Base
 scope :by_section, ->(section) { section.present? ? where(section: section) : all }
 scope :by_year,    ->(year)    { year.present?    ? where(year: year)       : all }
 scope :by_month,   ->(month)   { month.present?   ? where(month: month)     : all }
end

handle it via single route:

get '/:section(/:year(/:month))', to: 'articles#index'

and finally in views you could do:

<%= link_to 'In section 1', articles_path(section: 1) %>
<%= link_to 'In section 1 in 2014', articles_path(section: 1, year: 2014) %>

You may want to handle cases when user manipulate URL and i.e. put a string in place of year - it will probably result in error from db.

Personally I think it's perfectly fine to punish users manipulating URLs by hand with error page (500). Using constraints it would error anyways, but with 404 instead.

If you feel you really need to ensure that user is allowed to i.e. view only articles from selected sections you can add another scope:

scope :in_allowed_sections, -> { where(section: ['section 1', 'section 2']}

Given malicious user manipulating URL by hand this would result in valid SQL query:

where section in ('section 1', 'section 2') and section = 'section 3'

and user will receive a proper page (200) but without any articles displayed.

\$\endgroup\$
  • \$\begingroup\$ I tried this method and it turns up being just about as complex as the first method. Thanks for the suggestion though! \$\endgroup\$ – DaniG2k Dec 3 '14 at 11:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.