Sunday, May 1, 2011

Dynamic CMS like routes in ruby on rails

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?

From stackoverflow
  • 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.

  • 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.

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

  • 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
    
  • 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

  • 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

0 comments:

Post a Comment