views:

1018

answers:

6

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?

+1  A: 

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.

Squeegy
+3  A: 

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
A: 

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

+1  A: 

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
A: 

Doesn't work in rails3. My guess is that rails3 parses the entire block for " get '/blah/foo' statements" without using "map." . I really hate that magic there. How did you solve the problem? Maybe we should open a bug.

best,

-dennis

dennis
A: 

Hi, 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