Radiant CMS Database Form Extension

With a few tweaks, the Radiant CMS Mailer Extension can be turned into an extension that allows form submissions (with
pre-post validation) to save to a database; e.g.

class DatabaseFormPage < Page
 
  class DatabaseFormError < StandardError; end
 
  attr_reader :form_name, :form_conf, :form_error, :form_data, :tag_attr
    
  # Page processing. If the page has posted-back, it will try to save to contacts table
  # and redirect to a different page, if specified.
  def process(request, response)
    @request, @response = request, response
    @form_name, @form_error = nil, nil
    if request.post?
      @form_data = request.parameters[:database].to_hash
      # remove certain fields from hash
      form_data.delete("Submit")  
      form_data.delete("ignore")  
      @form_name = request.parameters[:form_name]
      @form_conf = config['form'][form_name].symbolize_keys || {}
      if save_form and form_conf.has_key? :success_page
        response.redirect( form_conf[:success_page] )
      else
        super(request, response) 
      end
    else
      super(request, response)
    end
  end
 
  # Save form data
  def save_form()
    begin
      contact = Contact.new(form_data.to_hash)
      contact.save
     rescue
      @form_error = "Error encountered while trying to submit form. #{$!}"
      return false
    end
    true
  end
 
 
  def cache?
    false
  end
  
  # DatabaseForm Tags:
    
    desc %{ This is just for creating the <r:database/> namespace. }
    tag "database" do |tag|
      tag.expand
    end
    
    desc %{    The <r:database:form name="X">...</r:database:form> tag is required.
    It should encompass all of the helper tags for creating form fields.}
    tag "database:form" do |tag|
      @tag_attr = { :class=>get_class_name('form') }.update( tag.attr.symbolize_keys )
      raise_error_if_name_missing 'database:form'
      # Build the html form tag...
      results = %Q(<script src="/javascripts/validation.js" type="text/javascript"></script>)
      results << %Q(<form id="contact-form-id" action="#{ url }" method="post" class="#{ tag_attr[:class] }" enctype="multipart/form-data">)
      results << %Q(<div><input type="hidden" name="form_name" value="#{ tag_attr[:name] }" /></div>)
      results << %Q(<div class="database-error">#{form_error}</div>) if form_error
      results << tag.expand
      results << %Q(</form>)
      results << %Q(<script>new Validation('contact-form-id');</script>)
 
    end
 
    # Build tags for all of the <input /> tags...
    %w(text password file submit reset checkbox radio hidden).each do |type|
      desc %{      Renders a #{type} form control for a database form.}
      tag "database:#{type}" do |tag|
        @tag_attr = tag.attr.symbolize_keys
        raise_error_if_name_missing "database:#{type}" unless %(submit reset).include? type
        input_tag_html( type )
      end      
    end
 
    desc %{    A <select>...</select> tag. This is compatible with the <r:database:option/> tag as well. }
    tag 'database:select' do |tag|
      @tag_attr = { :id=>tag.attr['name'], :class=>get_class_name('select'), :size=>'1' }.update( tag.attr.symbolize_keys )
      raise_error_if_name_missing "database:select"
      tag.locals.parent_tag_name = tag_attr[:name]
      tag.locals.parent_tag_type = 'select'
      results =  %Q(<select name="database[#{tag_attr[:name]}]" #{add_attrs_to("")}>)
      results << tag.expand
      results << "</select>"
    end
 
    desc %{ A <textarea>...</textarea> tag, of course }
    tag 'database:textarea' do |tag|
      @tag_attr = { :id=>tag.attr['name'], :class=>get_class_name('textarea'), :rows=>'5', :cols=>'35' }.update( tag.attr.symbolize_keys )
      raise_error_if_name_missing "database:textarea"
      results =  %Q(<textarea name="database[#{tag_attr[:name]}]" #{add_attrs_to("")}>)
      results << tag.expand
      results << "</textarea>"
    end
    
    %{    Special tag for radio groups. This one works with the <r:database:option/> tag }
    tag 'database:radiogroup' do |tag|
      @tag_attr = tag.attr.symbolize_keys
      raise_error_if_name_missing "database:radiogroup"
      tag.locals.parent_tag_name = tag_attr[:name]
      tag.locals.parent_tag_type = 'radiogroup'
      tag.expand
    end
 
    desc %{    This is a custom tag that will render an <option/> tag if the parent is a 
    <select>...</select> tag, or it will render an <input type="radio"/> tag if 
    the parent is a <r:database:radiogroup>...</r:database:radiogroup> }
    tag 'database:option' do |tag|
      @tag_attr = tag.attr.symbolize_keys
      raise_error_if_name_missing "database:option"
      result = ""
      if tag.locals.parent_tag_type == 'select'
        result << %Q|<option value="#{tag_attr.delete(:value) || tag_attr[:name]}" #{add_attrs_to("")}>#{tag_attr[:name]}</option>|
      elsif tag.locals.parent_tag_type == 'radiogroup'
        tag.globals.option_count = tag.globals.option_count.nil? ? 1 : tag.globals.option_count += 1
        options = tag_attr.clone.update({
          :id => "#{tag.locals.parent_tag_name}_#{tag.globals.option_count}",
          :value => tag_attr.delete(:value) || tag_attr[:name],
          :name => tag.locals.parent_tag_name
        })
        result << input_tag_html( 'radio', options )
        result << %Q|<label for="#{options[:id]}">#{tag_attr[:name]}</label>|
      end
    end
 
    desc %{    This is a custom tag that will render an obfuscated email address <option>
    using the email.js file. Use nested <r:address>...</r:address> to specify the email
    address and <r:label>...</r:label> to specify what the content of the tag should be. }
    tag 'database:email_option' do |tag|
      hash = tag.locals.params = {}
      contents = tag.expand
      address = hash['address'].blank? ? contents : hash['address']
      label = hash['label']
      if address =~ /([\w.%-]+)@([\w.-]+)\.([A-z]{2,4})/
        user, domain, tld = $1, $2, $3
        tld_num = TLDS.index(tld)
        unless label.blank?
        %{<script type="text/javascript">
              // <![CDATA[
              mail4('#{user}', '#{domain}', #{tld_num}, "#{label}");
              // ]]>
              </script>
        }
        else
        %{<script type="text/javascript">
              // <![CDATA[
              mail4('#{user}', '#{domain}', #{tld_num}, '#{user}');
              // ]]>
              </script>
        }
      end      
      end
    end
    
    tag "database:email_option:label" do |tag|
      tag.locals.params['label'] = tag.expand.strip
    end
  
    tag "database:email_option:address" do |tag|
      tag.locals.params['address'] = tag.expand.strip
    end
  
    
    desc %{    For use with email template parts -- retrieves the data posted by the form }
    tag 'database:get' do |tag|
      name = tag.attr['name']
      if name
        form_data[name].is_a?(Array) ? form_data[name].to_sentence : form_data[name]
      else
        form_data.to_hash.to_yaml.to_s
      end
    end  
 
protected
 
  # Since several form tags use the <input type="X" /> format, let's do that work in one place
  def input_tag_html(type, opts=tag_attr)
    options = { :id => tag_attr[:name], :value => "", :class=>get_class_name(type) }.update(opts)
    results =  %Q(<input type="#{type}" )
    results << %Q(name="database[#{options[:name]}]" ) if tag_attr[:name]
    results << "#{add_attrs_to("", options)}/>"
  end
  
  def add_attrs_to(results, tag_attrs=tag_attr)
    # Well, turns out I stringify the keys so I can sort them so I can test the tag output
    tag_attrs.stringify_keys.sort.each do |name, value|
      results << %Q(#{name.to_s}="#{value.to_s}" ) unless name == 'name'
    end
    results
  end
  
  # Get the default css class based on type
  def get_class_name(type, class_name=nil)
    class_name = 'database-form' if class_name.nil? and %(form).include? type
    class_name = 'database-field' if class_name.nil? and %(text password file select textarea).include? type
    class_name = 'database-button' if class_name.nil? and %(submit reset).include? type
    class_name = 'database-option' if class_name.nil? and %(checkbox radio).include? type
    class_name
  end
  
  # Raises a 'name missing' tag error
  def raise_name_error(tag_name)
    raise DatabaseFormTagError.new( "`#{tag_name}' tag requires a `name' attribute" )
  end
  def raise_error_if_name_missing(tag_name)
    raise_name_error( tag_name ) if tag_attr[:name].nil? or tag_attr[:name].empty?
  end
  
end

I
don’t like how it is hardcoded to use the contacts table. Any thoughts/ideas
on how to make the table more of dynamic, runtime setting? As you can imagine,
Contact is simply:

class Contact < ActiveRecord::Base
end

1 Comment »

  1. FYI, I just published a new extension, database form, that wraps your ideas here with some of my own and makes it easy for users to install. It’s available at http://github.com/zapnap/database_form if you want to take a look. Thanks for the code snippets and the inspiration, this was almost exactly what we needed for a recent project.

    Comment by nap — March 4, 2008 @ 1:49 pm

RSS feed for comments on this post. TrackBack URI

Leave a comment

ServiceCycle is a registered trademark of Supergloo, inc..