8

I want to create a CMS like site where the user starts off with a some generic pages, i.e.

  • homepage
  • about
  • contact
  • etc

and from there can add child pages dynamically, for example

  • homepage
    • articles
      • article1
        • something
          • something-else
      • article2
  • about
  • contact
  • etc

To achieve this I'm planning on using some kind of self-referential association like

class Page < ActiveRecord::Base
  belongs_to :parent, :class_name => 'Page'
  has_many :children, :class_name => 'Page'
end

The one thing I'm struggling with is the route generation. Because pages can be added on the fly I need to dynamically generate routes for these pages and there is no way of knowing how many levels deep a page may be nested

So if I start off with the homepage: /

and then start adding pages i.e.

/articles/article1/something/something-else/another-thing

How can something like that be achieved with the rails routing model?

Ryan Bigg
  • 106,965
  • 23
  • 235
  • 261
KJF
  • 2,083
  • 4
  • 21
  • 38

5 Answers5

11

Once you have some way to generate the URL string for your Page records (and I'll leave that part up to you), you can just map every page in config/routes.rb:

Page.all.each do |page|
  map.connect page.url, :controller => 'pages', :action => 'show', :id => page
end

And have an observer hook the page model to reload routes when something changes:

class PageObserver < ActiveRecord::Observer
  def reload_routes(page)
    ActionController::Routing::Routes.reload!
  end
  alias_method :after_save,    :reload_routes
  alias_method :after_destroy, :reload_routes
end

Don't forget to edit config/environment.rb to load the observer:

# Activate observers that should always be running
config.active_record.observers = :page_observer
kch
  • 77,385
  • 46
  • 136
  • 148
  • Thats a great solution, but how would you convert it to rails 3 'match' commands? – Rumpleteaser Mar 12 '12 at 09:09
  • 1
    If you're using rails 3, reload your routes with `MyApplication::Application.reload_routes!` – dhulihan May 24 '12 at 17:29
  • 1
    The problem I ran into with this method is that reloading routes at runtime only applies to the ruby process from which the observer methods are called. If you have multiple ruby processes (e.g. multiple unicorn workers), they won't pick up the new routes even though they'll be aware of the new records in the database. The result is that some visitors won't be able to access your new pages because the routes won't exist as far as the worker which is handling the request is concerned. The only solution would be to restart the whole application. I think. – russellb Mar 12 '13 at 18:42
  • As a followup to above, if you're using unicorn you could replace `ActionController::Routing::Routes.reload!` with `system("kill -USR2 \`cat #{Rails.root}/tmp/pids/unicorn.pid\`")` This will gracefully restart the Unicorn server so that all workers pick up the new routes. – russellb Mar 12 '13 at 19:48
8

One solution to this prob is to dynamically load routes from hooks on your models. From example, a snippet from the Slug model on my site:

class Slug < ActiveRecord::Base

  belongs_to :navigable

  validates_presence_of :name, :navigable_id
  validates_uniqueness_of :name

  after_save :update_route

  def add_route
    new_route = ActionController::Routing::Routes.builder.build(name, route_options)
    ActionController::Routing::Routes.routes.insert(0, new_route)
  end

  def remove_route
    ActionController::Routing::Routes.routes.reject! { |r| r.instance_variable_get(:@requirements)[:slug_id] == id }
  end

  def update_route
    remove_route
    add_route
  end

  def route_options
    @route_options ||= { :controller     => navigable.controller, 
                         :action         => navigable.action, 
                         :navigable_id   => navigable_id,
                         :slug_id        => id }
  end

end

This inserts the route at top priority (0 in the routing array in memory) after it has been saved.

Also, it sounds like you should be using a tree management plugin and like awesome nested set or better nested set to manage the tree for your site.

Ben Crouse
  • 8,290
  • 5
  • 35
  • 50
2

You have to parse the route yourself

map.connect '*url', :controller => 'pages', :action => 'show'

Now you should have a params[:url] available in your action that is the request path as an array separated by the slashes. Once you have those strings its a simple matter to find the models you need from there.

That was from memory, and it's been a long while. Hope it works for you.

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
0

I've implemented a similar functionality into a Rails gem, using self referential associations and a tree like js interface for reordering and nesting the "pages".

Templating language and authentication/authorization are left for the developer to implement. https://github.com/maca/tiny_cms

Macario
  • 2,214
  • 2
  • 22
  • 40
0

Look at RadiantCMS sources, they implement that functionality as far as i understand their self description.