Customizing RadiantCMS
with Extensions

Sean D. Cribbs

RadiantCMS Core Team

What is RadiantCMS?

From the website:

Radiant is a no-fluff, open source content management system designed for small teams.

From the website:

Radiant is a no-fluff, open source content management system designed for small teams.

In other words...

Not these:

Simple Admin Interface

Admin Interface

Pages

Page editing UI Site hierarchy

Snippets

Snippets

Layouts

Layouts

Radius Template Language

Radius template language

Behaviors and Caching

Behaviors Caching

Why use Radiant?

Why use Radiant?

Why use Radiant?

Radiant Sites (1/3)

ruby-lang.org

Radiant Sites (2/3)

diopa.org

Radiant Sites (3/3)

kckcc.edu

"OK, but what about...?"

OK, but what about...?

OK, but what about...?

OK, but what about...?

OK, but what about...?

It is possible

It is possible:
with Radiant 0.6
and Extensions

Radiant 0.5.x vs. 0.6.x

Radiant 0.5.x vs. 0.6.x

Radiant 0.5.x vs. 0.6.x

Radiant 0.5.x vs. 0.6.x

Radiant 0.5.x vs. 0.6.x

Radiant 0.5.x vs. 0.6.x

Radiant 0.5.x vs. 0.6.x

Radiant 0.5.x vs. 0.6.x

Radiant 0.5.x vs. 0.6.x

Radiant 0.5.x vs. 0.6.x

So what are extensions?

So what are extensions?

So what are extensions?

So what are extensions?

So what are extensions?

So what are extensions?

Create an extension

$ script/generate extension foo

Create an extension

$ script/generate extension foo
    create  vendor/extensions/foo/app/controllers
    create  vendor/extensions/foo/app/helpers
    create  vendor/extensions/foo/app/models
    create  vendor/extensions/foo/app/views
    create  vendor/extensions/foo/db/migrate
    create  vendor/extensions/foo/lib/tasks
    create  vendor/extensions/foo/test/functional
    create  vendor/extensions/foo/test/unit
    create  vendor/extensions/foo/README
    create  vendor/extensions/foo/Rakefile
    create  vendor/extensions/foo/foo_extension.rb
    create  vendor/extensions/foo/db/migrate/001_create_foo_extension_schema.rb
    create  vendor/extensions/foo/lib/tasks/foo_extension_tasks.rake
    create  vendor/extensions/foo/test/test_helper.rb
    create  vendor/extensions/foo/test/functional/foo_extension_test.rb

foo_extension.rb

class FooExtension < Radiant::Extension

  version "1.0"
  description "Describe your extension here"
  url "http://foo.com"

  # define_routes do |map|
  #   map.connect 'admin/foo/:action', :controller => 'admin/asset'
  # end
  
  def activate
    # admin.tabs.add "Foo", "/admin/foo", :after => "Layouts", :visibility => [:all]
  end
  
  def deactivate
    # admin.tabs.remove "Foo"
  end
    
end

foo_extension.rb

class FooExtension < Radiant::Extension

version "1.0" description "Describe your extension here" url "http://foo.com"
# define_routes do |map| # map.connect 'admin/foo/:action', :controller => 'admin/asset' # end def activate # admin.tabs.add "Foo", "/admin/foo", :after => "Layouts", :visibility => [:all] end def deactivate # admin.tabs.remove "Foo" end end

Extension "properties"

foo_extension.rb

class FooExtension < Radiant::Extension

  version "1.0"
  description "Describe your extension here"
  url "http://foo.com"

# define_routes do |map| # map.connect 'admin/foo/:action', :controller => 'admin/asset' # end
def activate # admin.tabs.add "Foo", "/admin/foo", :after => "Layouts", :visibility => [:all] end def deactivate # admin.tabs.remove "Foo" end end

Custom routes - just like routes.rb

foo_extension.rb

class FooExtension < Radiant::Extension

  version "1.0"
  description "Describe your extension here"
  url "http://foo.com"

  # define_routes do |map|
  #   map.connect 'admin/foo/:action', :controller => 'admin/asset'
  # end

def activate # admin.tabs.add "Foo", "/admin/foo", :after => "Layouts", :visibility => [:all] end
def deactivate # admin.tabs.remove "Foo" end end

Things to do on activation - add admin tabs, etc

foo_extension.rb

class FooExtension < Radiant::Extension

  version "1.0"
  description "Describe your extension here"
  url "http://foo.com"

  # define_routes do |map|
  #   map.connect 'admin/foo/:action', :controller => 'admin/asset'
  # end
  
  def activate
    # admin.tabs.add "Foo", "/admin/foo", :after => "Layouts", :visibility => [:all]
  end

def deactivate # admin.tabs.remove "Foo" end
end

Things to do on deactivation - remove tabs, etc

KCKCC Problem #1:

LDAP Directory Integration

LDAP Directory Integration

LDAP Directory Integration

Custom LDAP Wrapper

  • Abstract out the ugliness of ruby-ldap*
  • Store some defaults in Radiant::Config
  • Handle the connection "automagically"

*ActiveLDAP wouldn't work with Novell

Custom LDAP Wrapper

class LdapSystem
  #...
  def search(options = {})
    options.symbolize_keys!.reverse_merge! :base_dn => base_dn,
        :filter => "(objectClass=*)",
        :scope => LDAP::LDAP_SCOPE_SUBTREE,
        :attrs => ["dn", "givenName", "sn", "mail"],
        :sort => "sn"
 
    if options[:attrs].is_a? String
      options[:attrs] = options[:attrs].split(",").collect(&:strip)
    end
    connection.simple_bind(bind_user, bind_password) unless connection.bound?
results = connection.search2(options[:base_dn], options[:scope], options[:filter], options[:attrs]) rescue []
results.collect { |r| dearrayify(r) } end end

Custom Query Model

class LdapQuery < ActiveRecord::Base
  # ...
  def execute
    attrs = attributes.reject { |k,v|
        v.blank? or k.to_s == 'name'
        }.symbolize_keys
     LdapSystem.search(attrs)
  end
  # ...
end
  • DB stores all of the query parameters
  • LdapQuery#execute queries through our LdapSystem wrapper

Custom Admin UI

  • List all queries
  • Edit individual queries, with Ajax query testing
  • Edit global LDAP settings

Custom Admin UI - Create

Custom Admin UI - List

Custom global tags

Code

  tag "directory:query" do |tag|
    name = tag.attr['name']
    base, filter, attrs = tag.attr['base'], tag.attr['filter'], tag.attr['attrs']
    if name and query = LdapQuery.find_by_name(name)
      tag.locals.results = query.execute
      tag.expand unless tag.locals.results.empty?
    elsif filter
      query = LdapQuery.new :base_dn => base, :filter => filter, :attrs => attrs
      tag.locals.results = query.execute
      tag.expand unless tag.locals.results.empty?
    else
      raise TagError, "Must specify at least a filter on directory queries."
    end
  end

Usage

<r:directory:query name="Everyone"> ... </r:directory:query>

LDAP in Action

KCKCC Problem #2:

Front-End Concerns - Email Forms, Search

Already done for me! (almost)

  • Easy conversion from behaviors
    • Convert behaviors to Page subclasses
    • Move tag definitions into the Page subclass
    • Remove live-search
    • Fix Mailer bugs
  • Added some extra niceties
    • JS Email encryption for Mailer
    • truncate_and_strip_tags for Search

Mailer and Search Tricks

Pages that process user input must be uncached! (duh)

  # We need to process the page everytime, so that we can send the email!
  def cache?
    false
  end

Mailer and Search Tricks

Override Page#process method to handle POST

  def process(request, response)
    @request, @response = request, response
    @form_name, @form_error = nil, nil
    if request.post?
      @form_name = request.parameters[:mailer_name]
      @form_data = request.parameters[:mailer]
      @form_conf = config['mailers'][form_name].symbolize_keys || {}
      # If there are recipients defined, send email...
      if form_conf.has_key? :recipients
        if send_mail and form_conf.has_key? :redirect_to
          response.redirect( form_conf[:redirect_to] )
        else
          super(request, response) 
        end
      else
        @form_error = "Email wasn't sent because no recipients are defined"
        super(request, response)
      end
    else
      super(request, response)
    end
  end

KCKCC Problem #3:

The CRUD Apps - Events Calendar, Syllabi

CRUD Apps Overview

  • Admin UI for CRUD
  • Tag definitions
  • Models, controllers, views
  • Some Radiant "tricks"

Events Calendar Trick

Tags can be nested to scope events within multiple calendars

    tag "calendar" do |tag|
      name = tag.attr["name"].strip
      old_calendar = tag.locals.calendar
      old_events = tag.locals.events
      tag.locals.calendar = Calendar.find_by_name(name)
      if old_calendar
        old_events ||= old_calendar.events.find(:all,
            :order => "start_date asc, start_time asc")
        new_events = tag.locals.calendar.events.find(:all, 
            :order => "start_date asc, start_time asc")
        tag.locals.events = new_events & old_events
      end
      tag.expand
    end

(Event habtm :calendars)

Syllabi Tricks

  • "Virtual" pages can handle multiple URLs - FileNotFoundPage (404)
  • Prefix page defines URL space of child pages (sort of like Archive), referring any Prefix name to a Course page.
  • Course page is virtual and determines Prefix by the requested URL

Syllabi Tricks

Redefine URL space of children by redefining Page#find_by_url

  # Allows URLs to direct to the virtual course listing subpage
  def find_by_url(url, live = true, clean = false)
    url = clean_url(url) if clean
    if url =~ %r{#{self.url}(\w+)}
      prefix = $1
      children.find_by_class_name 'SyllabiCoursesPage'
    else
      super
    end
  end

Syllabi Tricks

Virtual page responds to multiple URLs and does something different for each

  def render
    lazy_initialize_parser_and_context
    @context.globals.prefix = Prefix.find_by_prefix(get_prefix_from_url)
    super
  end

  private
    def request_uri
      request.request_uri unless request.nil?
    end
  
    def get_prefix_from_url
      $1 if request_uri =~ %r{#{parent.url}/?(\w+)/?$}
    end

General Tips & Tricks

Radiant's Future

  • 0.6 - Lapidary (Extensions)
    • Complete extension system
    • Helpful tag, filter reference popups
    • Better UI mutability (facets?)
  • 0.7 - Intaglio (Blogging)
    • Comments, tagging
    • Mars Edit support
    • Convert from WordPress, Typo, Mephisto
    • Robust import/export

Questions?