Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

initial commit 0.2

  • Loading branch information...
commit d8262ed13de840a01ae884b5ffedd0cdb4d30f6c 0 parents
Nick Plante zapnap authored
100 README
... ... @@ -0,0 +1,100 @@
  1 += Database Form Extension for Radiant
  2 +
  3 +The Database Form extension enables Radiant users to build form pages that
  4 +will save visitor responses to a database table. Those responses can then
  5 +be exported for consumption in some other application (CRM, etc).
  6 +
  7 +Install the extension, run the rake radiant:extensions:database_form:migrate
  8 +task, and the rake radiant:extensions:database_form:update task. These
  9 +tasks will add the necessary database table to your Radiant installation
  10 +and copy over the JavaScript that's required for client-side validation.
  11 +
  12 +Once you've done this, log into the Radiant admin and set the page type
  13 +of the page where you'll build the form to DatabaseFormPage. Then create
  14 +your form using tags. Here's an example of usage:
  15 +
  16 + <r:database:form name="requestinfo" redirect_to="/contact/thank-you">
  17 + Name:<br/>
  18 + <r:database:text name="name"/><br/>
  19 + Email:<br/>
  20 + <r:database:text name="email"/><br/>
  21 + Primary Interest:<br/>
  22 + <r:database:select name="primary_interest">
  23 + <r:database:option name="work"/>
  24 + <r:database:option name="fun!"/>
  25 + </r:database:select><br/>
  26 + <r:database:submit value="Submit"/>
  27 + <r:database:reset value="Reset"/>
  28 + </r:database:form>
  29 +
  30 +As shown above, you can use the redirect_to attribute to specify where the
  31 +visitor should be redirected to after submitting the form.
  32 +
  33 +The entire inventory of tags includes:
  34 +
  35 + <r:database:form name="" redirect_to="" validate="">...</r:database:form>
  36 + <r:database:text name="" validate=""/>
  37 + <r:database:password name="" validate=""/>
  38 + <r:database:checkbox name="" validate=""/>
  39 + <r:database:hidden name="" validate=""/>
  40 + <r:database:textarea name="" validate=""/>
  41 + <r:database:radio name="" validate=""/>
  42 + <r:database:radiogroup name="" validate="">...</r:database:radiogroup>
  43 + <r:database:select name="" validate="">...</r:database:select>
  44 + <r:database:option name=""/>
  45 + <r:database:us_states/>
  46 + <r:database:ca_provinces/>
  47 + <r:database:countries/>
  48 + <r:database:submit/>
  49 + <r:database:reset/>
  50 +
  51 +Note that most tags support client-side validation. Thanks to Andrew Tetlaw
  52 +for his spiffy Prototype-based validation JavaScript. If you wish to include
  53 +validation in your form, you must set the validate attribute in the form tag.
  54 +You can then choose from a variety of validation routines for the form
  55 +elements, including: required, validate-number, validate-digits,
  56 +validate-alpha, validate-alphanum, validate-date, validate-email,
  57 +validate-url, validate-currency-dollar, validate-selection, and
  58 +validate-one-required. You'll also need to include prototype in your layout,
  59 +of course (IMPORTANT; without it validation won't work).
  60 +
  61 +<r:database:form name="contact" validate="true">
  62 + <r:database:text name="name" validate="required"/>
  63 + <r:database:text name="email" validate="required validate-email"/>
  64 +</r:database:form>
  65 +
  66 +If you care to, you can style these validations using CSS:
  67 +
  68 +input.validation-failed, textarea.validation-failed {
  69 + border: 1px solid #900;
  70 + color: #900;
  71 +}
  72 +.validation-advice {
  73 + margin: 5px 0;
  74 + padding: 5px;
  75 + background-color: #900;
  76 + color: #FFF;
  77 + font-weight: bold;
  78 +}
  79 +
  80 +The Database Forms extension also adds a tab to Radiant's admin
  81 +interface which can be used to export user form data. You can select
  82 +by form name (the name you assigned to a form in its tag) and date
  83 +ranges. XML is the only supported export format at this time.
  84 +
  85 +Maybe we'll sexy this up some day and make listings browsable through
  86 +the admin but the most common use case is probably exporting data, and
  87 +that's what we needed at the time, so it is what it is. If you'd like
  88 +to extend it, we definitely welcome submissions.
  89 +
  90 +This extension is based on the Mailer extension by Matt McCray and
  91 +(originally) Sean Cribbs. It was inspired by a post on SuperGloo's blog
  92 +http://code.supergloo.com/2007/12/12/radiant-cms-database-form-extension
  93 +but should be a bit more flexible than what was described there,
  94 +allowing you to add arbitrary forms at will without adding database
  95 +tables (model attributes for the FormResponse model are serialized
  96 +and stored in a content text area).
  97 +
  98 +Tested on Radiant 0.6.4
  99 +
  100 +http://ubikorp.com/projects
25 Rakefile
... ... @@ -0,0 +1,25 @@
  1 +require 'rake'
  2 +require 'rake/testtask'
  3 +require 'rake/rdoctask'
  4 +
  5 +desc 'Default: run unit tests.'
  6 +task :default => :test
  7 +
  8 +desc 'Test the database form extension.'
  9 +Rake::TestTask.new(:test) do |t|
  10 + t.libs << 'lib'
  11 + t.pattern = 'test/**/*_test.rb'
  12 + t.verbose = true
  13 +end
  14 +
  15 +desc 'Generate documentation for the database form extension.'
  16 +Rake::RDocTask.new(:rdoc) do |rdoc|
  17 + rdoc.rdoc_dir = 'rdoc'
  18 + rdoc.title = 'DatabaseFormExtension'
  19 + rdoc.options << '--line-numbers' << '--inline-source'
  20 + rdoc.rdoc_files.include('README')
  21 + rdoc.rdoc_files.include('lib/**/*.rb')
  22 +end
  23 +
  24 +# Load any custom rakefiles for extension
  25 +Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }
32 app/controllers/admin/form_responses_controller.rb
... ... @@ -0,0 +1,32 @@
  1 +require 'ostruct'
  2 +
  3 +class Admin::FormResponsesController < ApplicationController
  4 + def index
  5 + @filter = OpenStruct.new(:name => "", :start_time => 7.days.ago, :end_time => Time.now)
  6 + @form_names = FormResponse.find_by_sql("SELECT DISTINCT name FROM form_responses ORDER BY name").map { |fr| fr.name }
  7 + end
  8 +
  9 + def export
  10 + options = { :order => "name, created_at" }
  11 + if params[:filter]
  12 + conditions = []
  13 + conditions << "name = '#{params[:filter][:name]}'" if params[:filter][:name]
  14 + conditions << "created_at >= '#{build_datetime_from_params(:start_time, params[:filter])}'" if params[:filter]['start_time(1i)']
  15 + conditions << "created_at <= '#{build_datetime_from_params(:end_time, params[:filter])}'" if params[:filter]['end_time(1i)']
  16 + options[:conditions] = conditions.join(" AND ")
  17 + end
  18 + @form_responses = FormResponse.find(:all, options)
  19 + render(:xml => @form_responses.to_xml(:root => "form-responses"))
  20 + end
  21 +
  22 + protected
  23 +
  24 + # Reconstruct a datetime object from datetime_select helper form params
  25 + def build_datetime_from_params(field_name, params)
  26 + DateTime.new(params["#{field_name.to_s}(1i)"].to_i,
  27 + params["#{field_name.to_s}(2i)"].to_i,
  28 + params["#{field_name.to_s}(3i)"].to_i,
  29 + params["#{field_name.to_s}(41)"].to_i,
  30 + params["#{field_name.to_s}(5i)"].to_i).strftime('%Y-%m-%d %H:%M')
  31 + end
  32 +end
2  app/helpers/admin/form_responses_helper.rb
... ... @@ -0,0 +1,2 @@
  1 +module Admin::FormResponsesHelper
  2 +end
480 app/models/database_form_page.rb
... ... @@ -0,0 +1,480 @@
  1 +class DatabaseFormPage < Page
  2 + class DatabaseFormTagError < StandardError; end
  3 +
  4 + attr_reader :form_name, :form_error, :form_data, :tag_attr
  5 +
  6 + # Page processing. If the page has posted-back, it will try to save to contacts
  7 + # table and redirect to a different page, if specified.
  8 + def process(request, response)
  9 + @request, @response = request, response
  10 + @form_name, @form_error = nil, nil
  11 + if request.post?
  12 + @form_data = request.parameters[:content].to_hash
  13 +
  14 + # Remove certain fields from hash
  15 + form_data.delete("Submit")
  16 + form_data.delete("Ignore")
  17 + form_data.delete_if { |key, value| key.match(/_verify$/) }
  18 +
  19 + @form_name = request.parameters[:form_name]
  20 + redirect_to = request.parameters[:redirect_to]
  21 +
  22 + if save_form and redirect_to
  23 + response.redirect(redirect_to)
  24 + else
  25 + super(request, response)
  26 + end
  27 + else
  28 + super(request, response)
  29 + end
  30 + end
  31 +
  32 + # Save form data
  33 + def save_form
  34 + form_response = FormResponse.new(:name => form_name)
  35 + form_response.content = form_data
  36 + if !form_response.save
  37 + @form_error = "Error encountered while trying to submit form. #{$!}"
  38 + false
  39 + else
  40 + true
  41 + end
  42 + end
  43 +
  44 + # Don't cache this page!
  45 + def cache?
  46 + false
  47 + end
  48 +
  49 + # DatabaseForm Tags:
  50 +
  51 + desc %{
  52 + Creates the @<r:database/>@ namespace See @<r:database:form>...</r:database:form>@.
  53 + }
  54 + tag 'database' do |tag|
  55 + tag.expand
  56 + end
  57 +
  58 + desc %{
  59 + The @<r:database:form>...</r:database:form>@ should include all of the
  60 + helper tags for creating form fields. The @name@ attribute is required. The
  61 + @return_to@ attribute can be used to specify an optional return URL that the user
  62 + will be redirected to after submission. The @validate@ attribute can be used to
  63 + enable client-side JavaScript form validation. Individual helper tags must declare
  64 + the kind of validation that should be performed on them using a @validate@
  65 + attribute as well (possible values: @required@, @validate-number@, @validate-digits@,
  66 + @validate-alpha@, @validate-alphanum@, @validate-date@, @validate-email@,
  67 + @validate-url@, @validate-currency-dollar@, @validate-selection@,
  68 + @validate-one-required@).
  69 +
  70 + *Usage:*
  71 + <pre><code><r:database:form name="contact" return_to="/contact/thank-you" validate="true">
  72 + <r:text name="name" validate="required validate-alpha"/>
  73 + <r:text name="email" validate="required validate-email"/>
  74 + <r:submit/>
  75 + </r:database:form></code></pre>
  76 + }
  77 + tag 'database:form' do |tag|
  78 + @tag_attr = tag.attr.symbolize_keys
  79 + tag.locals.validate = tag_attr[:validate]
  80 + raise_error_if_name_missing("database:form")
  81 +
  82 + # Build the html form tag...
  83 + results = %Q(<form action="#{url}" method="post" class="#{tag_attr[:class]}" enctype="multipart/form-data" id="#{tag_attr[:name]}" name="#{tag_attr[:name]}">)
  84 +
  85 + if tag.locals.validate
  86 + results << %Q(<script src="/javascripts/validation.js" type="text/javascript"></script>)
  87 + results << %Q(<script type="text/javascript">)
  88 + results << %Q(function formCallback(result, form) { if (result == true) { $('#{tag_attr[:name]}').submit(); } })
  89 + results << %Q(var valid = new Validation('#{tag_attr[:name]}', { immediate: false, onFormValidate: formCallback });)
  90 + results << %Q(</script>)
  91 + end
  92 +
  93 + results << %Q(<input type="hidden" name="form_name" value="#{tag_attr[:name]}" />)
  94 + results << %Q(<input type="hidden" name="redirect_to" value="#{tag_attr[:redirect_to]}" />) unless tag_attr[:redirect_to].nil?
  95 + results << %Q(<div class="database-error">#{form_error}</div>) if form_error
  96 + results << tag.expand
  97 + results << %Q(</form>)
  98 + end
  99 +
  100 + # Build tags for all of the <input /> tags...
  101 + %w(text password file submit reset checkbox radio hidden).each do |type|
  102 + desc %{
  103 + Renders a @<#{type}>@ form control for a database form.
  104 + }
  105 + tag "database:#{type}" do |tag|
  106 + @tag_attr = tag.attr.symbolize_keys
  107 + raise_error_if_name_missing("database:#{type}") unless %(submit reset).include?(type)
  108 + @tag_attr[:onclick] = "valid.validate(); return false" if tag.locals.validate and type == 'submit'
  109 + @tag_attr[:onclick] = "valid.reset();" if tag.locals.validate and type == 'reset'
  110 + input_tag_html(type)
  111 + end
  112 + end
  113 +
  114 + desc %{
  115 + Renders a @<select>...</select>@ form control. This is used with the
  116 + the @<r:database:option/>@ tag to build selection lists.
  117 + }
  118 + tag 'database:select' do |tag|
  119 + @tag_attr = { :id => tag.attr['name'], :size => '1' }.update(tag.attr.symbolize_keys)
  120 + raise_error_if_name_missing("database:select")
  121 + tag.locals.parent_tag_name = tag_attr[:name]
  122 + tag.locals.parent_tag_type = "select"
  123 + results = %Q(<select name="content[#{tag_attr[:name]}]" #{add_attrs_to("")}>)
  124 + results << tag.expand
  125 + results << "</select>"
  126 + end
  127 +
  128 + desc %{
  129 + Renders a @<textarea>...</textarea>@ form control.
  130 + }
  131 + tag 'database:textarea' do |tag|
  132 + @tag_attr = { :id => tag.attr['name'], :rows => '5', :cols => '35' }.update(tag.attr.symbolize_keys)
  133 + raise_error_if_name_missing("database:textarea")
  134 + results = %Q(<textarea name="content[#{tag_attr[:name]}]" #{add_attrs_to("")}>)
  135 + results << tag.expand
  136 + results << "</textarea>"
  137 + end
  138 +
  139 + %{
  140 + Special tag for radio groups. Works with the @<r:database:option/>@ tag.
  141 + }
  142 + tag 'database:radiogroup' do |tag|
  143 + @tag_attr = tag.attr.symbolize_keys
  144 + raise_error_if_name_missing("database:radiogroup")
  145 + tag.locals.parent_tag_name = tag_attr[:name]
  146 + tag.locals.parent_tag_type = 'radiogroup'
  147 + tag.expand
  148 + end
  149 +
  150 + desc %{
  151 + Custom tag for rendering an @<option/>@ tag if the parent is a
  152 + @<select>...</select>@ tag, or rendering an @<input type="radio"/>@ tag
  153 + if the parent is a @<r:database:radiogroup>...</r:database:radiogroup>@.
  154 + }
  155 + tag 'database:option' do |tag|
  156 + @tag_attr = tag.attr.symbolize_keys
  157 + raise_error_if_name_missing("database:option")
  158 + result = ""
  159 + if tag.locals.parent_tag_type == 'select'
  160 + result << %Q(<option value="#{tag_attr.delete(:value) || tag_attr[:name]}" #{add_attrs_to("")}>#{tag_attr[:name]}</option>)
  161 + elsif tag.locals.parent_tag_type == 'radiogroup'
  162 + tag.globals.option_count = tag.globals.option_count.nil? ? 1 : tag.globals.option_count += 1
  163 + options = tag_attr.clone.update({
  164 + :id => "#{tag.locals.parent_tag_name}_#{tag.globals.option_count}",
  165 + :value => tag_attr.delete(:value) || tag_attr[:name],
  166 + :name => tag.locals.parent_tag_name
  167 + })
  168 + result << input_tag_html('radio', options)
  169 + result << %Q(<label for="#{options[:id]}">#{tag_attr[:name]}</label>)
  170 + end
  171 + end
  172 +
  173 + desc %{
  174 + Renders an option list of US states. Use between the appropriate
  175 + @<select>...<select/>@ tags.
  176 + }
  177 + tag 'database:us_states' do |tag|
  178 + results = %Q(<option value="AL">AL</option>)
  179 + results << %Q(<option value="AK">AK</option>)
  180 + results << %Q(<option value="AZ">AZ</option>)
  181 + results << %Q(<option value="AR">AR</option>)
  182 + results << %Q(<option value="CA">CA</option>)
  183 + results << %Q(<option value="CO">CO</option>)
  184 + results << %Q(<option value="CT">CT</option>)
  185 + results << %Q(<option value="DE">DE</option>)
  186 + results << %Q(<option value="DC">DC</option>)
  187 + results << %Q(<option value="FL">FL</option>)
  188 + results << %Q(<option value="GA">GA</option>)
  189 + results << %Q(<option value="HI">HI</option>)
  190 + results << %Q(<option value="ID">ID</option>)
  191 + results << %Q(<option value="IL">IL</option>)
  192 + results << %Q(<option value="IN">IN</option>)
  193 + results << %Q(<option value="IA">IA</option>)
  194 + results << %Q(<option value="KS">KS</option>)
  195 + results << %Q(<option value="KY">KY</option>)
  196 + results << %Q(<option value="LA">LA</option>)
  197 + results << %Q(<option value="ME">ME</option>)
  198 + results << %Q(<option value="MD">MD</option>)
  199 + results << %Q(<option value="MA">MA</option>)
  200 + results << %Q(<option value="MI">MI</option>)
  201 + results << %Q(<option value="MN">MS</option>)
  202 + results << %Q(<option value="MS">MS</option>)
  203 + results << %Q(<option value="MO">MO</option>)
  204 + results << %Q(<option value="MT">MT</option>)
  205 + results << %Q(<option value="NE">NE</option>)
  206 + results << %Q(<option value="NV">NV</option>)
  207 + results << %Q(<option value="NH">NH</option>)
  208 + results << %Q(<option value="NJ">NJ</option>)
  209 + results << %Q(<option value="NM">NM</option>)
  210 + results << %Q(<option value="NY">NY</option>)
  211 + results << %Q(<option value="NC">NC</option>)
  212 + results << %Q(<option value="ND">ND</option>)
  213 + results << %Q(<option value="OH">OH</option>)
  214 + results << %Q(<option value="OK">OK</option>)
  215 + results << %Q(<option value="OR">OR</option>)
  216 + results << %Q(<option value="PA">PA</option>)
  217 + results << %Q(<option value="RI">RI</option>)
  218 + results << %Q(<option value="SC">SC</option>)
  219 + results << %Q(<option value="SD">SD</option>)
  220 + results << %Q(<option value="TN">TN</option>)
  221 + results << %Q(<option value="TX">TX</option>)
  222 + results << %Q(<option value="UT">UT</option>)
  223 + results << %Q(<option value="VT">VT</option>)
  224 + results << %Q(<option value="VA">VA</option>)
  225 + results << %Q(<option value="WA">WA</option>)
  226 + results << %Q(<option value="WV">WV</option>)
  227 + results << %Q(<option value="WI">WI</option>)
  228 + results << %Q(<option value="WY">WY</option>)
  229 + end
  230 +
  231 + desc %{
  232 + Renders an option list of Canadian provinces. Use between appropriate
  233 + @<select>...</select>@ tags.
  234 + }
  235 + tag 'database:ca_provinces' do |tag|
  236 + results = %Q(<option value="AB">AB</option>)
  237 + results << %Q(<option value="BC">BC</option>)
  238 + results << %Q(<option value="MB">MB</option>)
  239 + results << %Q(<option value="NB">NB</option>)
  240 + results << %Q(<option value="NL">NL</option>)
  241 + results << %Q(<option value="NT">NT</option>)
  242 + results << %Q(<option value="NU">NU</option>)
  243 + results << %Q(<option value="ON">ON</option>)
  244 + results << %Q(<option value="PE">PE</option>)
  245 + results << %Q(<option value="QC">QC</option>)
  246 + results << %Q(<option value="SK">SK</option>)
  247 + results << %Q(<option value="YT">YT</option>)
  248 + end
  249 +
  250 + desc %{
  251 + Renders an option list of countries. Use between appropriate
  252 + @<select>...</select>@ tags.
  253 + }
  254 + tag 'database:countries' do |tag|
  255 + results = %Q{<option value="United States">United States</option>}
  256 + results << %Q{<option value="Afghanistan">Afghanistan</option>}
  257 + results << %Q{<option value="Albania">Albania</option>}
  258 + results << %Q{<option value="Algeria">Algeria</option>}
  259 + results << %Q{<option value="American Samoa">American Samoa</option>}
  260 + results << %Q{<option value="Andorra">Andorra</option>}
  261 + results << %Q{<option value="Angola">Angola</option>}
  262 + results << %Q{<option value="Anguilla">Anguilla</option>}
  263 + results << %Q{<option value="Antarctica">Antarctica</option>}
  264 + results << %Q{<option value="Antigua and Barbuda">Antigua and Barbuda</option>}
  265 + results << %Q{<option value="Argentina">Argentina</option>}
  266 + results << %Q{<option value="Armenia">Armenia</option>}
  267 + results << %Q{<option value="Aruba">Aruba</option>}
  268 + results << %Q{<option value="Australia">Australia</option>}
  269 + results << %Q{<option value="Austria">Austria</option>}
  270 + results << %Q{<option value="Azerbaijan">Azerbaijan</option>}
  271 + results << %Q{<option value="Bahamas">Bahamas</option>}
  272 + results << %Q{<option value="Bahrain">Bahrain</option>}
  273 + results << %Q{<option value="Bangladesh">Bangladesh</option>}
  274 + results << %Q{<option value="Barbados">Barbados</option>}
  275 + results << %Q{<option value="Belarus">Belarus</option>}
  276 + results << %Q{<option value="Belgium">Belgium</option>}
  277 + results << %Q{<option value="Belize">Belize</option>}
  278 + results << %Q{<option value="Benin">Benin</option>}
  279 + results << %Q{<option value="Bermuda">Bermuda</option>}
  280 + results << %Q{<option value="Bhutan">Bhutan</option>}
  281 + results << %Q{<option value="Bolivia">Bolivia</option>}
  282 + results << %Q{<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>}
  283 + results << %Q{<option value="Botswana">Botswana</option>}
  284 + results << %Q{<option value="Brazil">Brazil</option>}
  285 + results << %Q{<option value="Brunei">Brunei</option>}
  286 + results << %Q{<option value="Bulgaria">Bulgaria</option>}
  287 + results << %Q{<option value="Burkina Faso">Burkina Faso</option>}
  288 + results << %Q{<option value="Burundi">Burundi</option>}
  289 + results << %Q{<option value="Cambodia">Cambodia</option>}
  290 + results << %Q{<option value="Cameroon">Cameroon</option>}
  291 + results << %Q{<option value="Canada">Canada</option>}
  292 + results << %Q{<option value="Cape Verde">Cape Verde</option>}
  293 + results << %Q{<option value="Cayman Islands">Cayman Islands</option>}
  294 + results << %Q{<option value="Central African Republic">Central African Republic</option>}
  295 + results << %Q{<option value="Chad">Chad</option>}
  296 + results << %Q{<option value="Chile">Chile</option>}
  297 + results << %Q{<option value="China">China</option>}
  298 + results << %Q{<option value="Colombia">Colombia</option>}
  299 + results << %Q{<option value="Congo">Congo</option>}
  300 + results << %Q{<option value="Cook Islands">Cook Islands</option>}
  301 + results << %Q{<option value="Costa Rica">Costa Rica</option>}
  302 + results << %Q{<option value="Cote d\'Ivoire">Cote d\'Ivoire</option>}
  303 + results << %Q{<option value="Croatia (Hrvatska)">Croatia (Hrvatska)</option>}
  304 + results << %Q{<option value="Cyprus">Cyprus</option>}
  305 + results << %Q{<option value="Czech Republic">Czech Republic</option>}
  306 + results << %Q{<option value="Denmark">Denmark</option>}
  307 + results << %Q{<option value="Djibouti">Djibouti</option>}
  308 + results << %Q{<option value="Dominica">Dominica</option>}
  309 + results << %Q{<option value="Dominican Republic">Dominican Republic</option>}
  310 + results << %Q{<option value="East Timor">East Timor</option>}
  311 + results << %Q{<option value="Ecuador">Ecuador</option>}
  312 + results << %Q{<option value="Egypt">Egypt</option>}
  313 + results << %Q{<option value="El Salvador">El Salvador</option>}
  314 + results << %Q{<option value="Equatorial Guinea">Equatorial Guinea</option>}
  315 + results << %Q{<option value="Eritrea">Eritrea</option>}
  316 + results << %Q{<option value="Estonia">Estonia</option>}
  317 + results << %Q{<option value="Ethiopia">Ethiopia</option>}
  318 + results << %Q{<option value="Falkland Islands">Falkland Islands</option>}
  319 + results << %Q{<option value="Fiji">Fiji</option>}
  320 + results << %Q{<option value="Finland">Finland</option>}
  321 + results << %Q{<option value="France">France</option>}
  322 + results << %Q{<option value="French Guiana">French Guiana</option>}
  323 + results << %Q{<option value="French Polynesia">French Polynesia</option>}
  324 + results << %Q{<option value="Gabon">Gabon</option>}
  325 + results << %Q{<option value="Gambia">Gambia</option>}
  326 + results << %Q{<option value="Georgia">Georgia</option>}
  327 + results << %Q{<option value="Germany">Germany</option>}
  328 + results << %Q{<option value="Ghana">Ghana</option>}
  329 + results << %Q{<option value="Gibraltar">Gibraltar</option>}
  330 + results << %Q{<option value="Greece">Greece</option>}
  331 + results << %Q{<option value="Greenland">Greenland</option>}
  332 + results << %Q{<option value="Grenada">Grenada</option>}
  333 + results << %Q{<option value="Guadeloupe">Guadeloupe</option>}
  334 + results << %Q{<option value="Guam">Guam</option>}
  335 + results << %Q{<option value="Guatemala">Guatemala</option>}
  336 + results << %Q{<option value="Guinea">Guinea</option>}
  337 + results << %Q{<option value="Guinea-Bissau">Guinea-Bissau</option>}
  338 + results << %Q{<option value="Guyana">Guyana</option>}
  339 + results << %Q{<option value="Haiti ">Haiti </option>}
  340 + results << %Q{<option value="Honduras">Honduras</option>}
  341 + results << %Q{<option value="Hong Kong">Hong Kong</option>}
  342 + results << %Q{<option value="Hungary">Hungary</option>}
  343 + results << %Q{<option value="Iceland">Iceland</option>}
  344 + results << %Q{<option value="India">India</option>}
  345 + results << %Q{<option value="Indonesia">Indonesia</option>}
  346 + results << %Q{<option value="Iran">Iran</option>}
  347 + results << %Q{<option value="Iraq">Iraq</option>}
  348 + results << %Q{<option value="Ireland">Ireland</option>}
  349 + results << %Q{<option value="Israel">Israel</option>}
  350 + results << %Q{<option value="Italy">Italy</option>}
  351 + results << %Q{<option value="Jamaica">Jamaica</option>}
  352 + results << %Q{<option value="Japan">Japan</option>}
  353 + results << %Q{<option value="Jordan">Jordan</option>}
  354 + results << %Q{<option value="Kazakhstan">Kazakhstan</option>}
  355 + results << %Q{<option value="Kenya">Kenya</option>}
  356 + results << %Q{<option value="Kiribati">Kiribati</option>}
  357 + results << %Q{<option value="South Korea">South Korea</option>}
  358 + results << %Q{<option value="Kuwait">Kuwait</option>}
  359 + results << %Q{<option value="Kyrgyzstan">Kyrgyzstan</option>}
  360 + results << %Q{<option value="Laos">Laos</option>}
  361 + results << %Q{<option value="Latvia">Latvia</option>}
  362 + results << %Q{<option value="Lebanon">Lebanon</option>}
  363 + results << %Q{<option value="Lesotho">Lesotho</option>}
  364 + results << %Q{<option value="Liberia">Liberia</option>}
  365 + results << %Q{<option value="Libya">Libya</option>}
  366 + results << %Q{<option value="Liechtenstein">Liechtenstein</option>}
  367 + results << %Q{<option value="Lithuania">Lithuania</option>}
  368 + results << %Q{<option value="Luxembourg">Luxembourg</option>}
  369 + results << %Q{<option value="Macau">Macau</option>}
  370 + results << %Q{<option value="Macedonia">Macedonia</option>}
  371 + results << %Q{<option value="Madagascar">Madagascar</option>}
  372 + results << %Q{<option value="Malawi">Malawi</option>}
  373 + results << %Q{<option value="Malaysia">Malaysia</option>}
  374 + results << %Q{<option value="Maldives">Maldives</option>}
  375 + results << %Q{<option value="Mali">Mali</option>}
  376 + results << %Q{<option value="Malta">Malta</option>}
  377 + results << %Q{<option value="Martinique">Martinique</option>}
  378 + results << %Q{<option value="Mauritania">Mauritania</option>}
  379 + results << %Q{<option value="Mauritius">Mauritius</option>}
  380 + results << %Q{<option value="Mexico">Mexico</option>}
  381 + results << %Q{<option value="Micronesia">Micronesia</option>}
  382 + results << %Q{<option value="Moldova">Moldova</option>}
  383 + results << %Q{<option value="Monaco">Monaco</option>}
  384 + results << %Q{<option value="Mongolia">Mongolia</option>}
  385 + results << %Q{<option value="Montenegro">Montenegro</option>}
  386 + results << %Q{<option value="Montserrat">Montserrat</option>}
  387 + results << %Q{<option value="Morocco">Morocco</option>}
  388 + results << %Q{<option value="Mozambique">Mozambique</option>}
  389 + results << %Q{<option value="Namibia">Namibia</option>}
  390 + results << %Q{<option value="Nepal">Nepal</option>}
  391 + results << %Q{<option value="Netherlands">Netherlands</option>}
  392 + results << %Q{<option value="New Zealand">New Zealand</option>}
  393 + results << %Q{<option value="Nicaragua">Nicaragua</option>}
  394 + results << %Q{<option value="Niger">Niger</option>}
  395 + results << %Q{<option value="Nigeria">Nigeria</option>}
  396 + results << %Q{<option value="Norway">Norway</option>}
  397 + results << %Q{<option value="Oman">Oman</option>}
  398 + results << %Q{<option value="Pakistan">Pakistan</option>}
  399 + results << %Q{<option value="Panama">Panama</option>}
  400 + results << %Q{<option value="Papua New Guinea">Papua New Guinea</option>}
  401 + results << %Q{<option value="Paraguay">Paraguay</option>}
  402 + results << %Q{<option value="Peru">Peru</option>}
  403 + results << %Q{<option value="Philippines">Philippines</option>}
  404 + results << %Q{<option value="Poland">Poland</option>}
  405 + results << %Q{<option value="Portugal">Portugal</option>}
  406 + results << %Q{<option value="Puerto Rico">Puerto Rico</option>}
  407 + results << %Q{<option value="Qatar">Qatar</option>}
  408 + results << %Q{<option value="Romania">Romania</option>}
  409 + results << %Q{<option value="Russia">Russia</option>}
  410 + results << %Q{<option value="Rwanda">Rwanda</option>}
  411 + results << %Q{<option value="Saudi Arabia">Saudi Arabia</option>}
  412 + results << %Q{<option value="Senegal">Senegal</option>}
  413 + results << %Q{<option value="Serbia">Serbia</option>}
  414 + results << %Q{<option value="Seychelles">Seychelles</option>}
  415 + results << %Q{<option value="Sierra Leone">Sierra Leone</option>}
  416 + results << %Q{<option value="Singapore">Singapore</option>}
  417 + results << %Q{<option value="Slovakia">Slovakia</option>}
  418 + results << %Q{<option value="Slovenia">Slovenia</option>}
  419 + results << %Q{<option value="Somalia">Somalia</option>}
  420 + results << %Q{<option value="South Africa">South Africa</option>}
  421 + results << %Q{<option value="Spain">Spain</option>}
  422 + results << %Q{<option value="Sri Lanka">Sri Lanka</option>}
  423 + results << %Q{<option value="Sudan">Sudan</option>}
  424 + results << %Q{<option value="Suriname">Suriname</option>}
  425 + results << %Q{<option value="Swaziland">Swaziland</option>}
  426 + results << %Q{<option value="Sweden">Sweden</option>}
  427 + results << %Q{<option value="Switzerland">Switzerland</option>}
  428 + results << %Q{<option value="Syria">Syria</option>}
  429 + results << %Q{<option value="Taiwan">Taiwan</option>}
  430 + results << %Q{<option value="Tajikistan">Tajikistan</option>}
  431 + results << %Q{<option value="Tanzania">Tanzania</option>}
  432 + results << %Q{<option value="Thailand">Thailand</option>}
  433 + results << %Q{<option value="The Vatican">The Vatican</option>}
  434 + results << %Q{<option value="Togo">Togo</option>}
  435 + results << %Q{<option value="Trinidad and Tobago">Trinidad and Tobago</option>}
  436 + results << %Q{<option value="Tunisia">Tunisia</option>}
  437 + results << %Q{<option value="Turkey">Turkey</option>}
  438 + results << %Q{<option value="Turkmenistan">Turkmenistan</option>}
  439 + results << %Q{<option value="Uganda">Uganda</option>}
  440 + results << %Q{<option value="Ukraine">Ukraine</option>}
  441 + results << %Q{<option value="United Arab Emirates">United Arab Emirates</option>}
  442 + results << %Q{<option value="United Kingdom">United Kingdom</option>}
  443 + results << %Q{<option value="Uruguay">Uruguay</option>}
  444 + results << %Q{<option value="Uzbekistan">Uzbekistan</option>}
  445 + results << %Q{<option value="Venezuela">Venezuela</option>}
  446 + results << %Q{<option value="Viet Nam">Viet Nam</option>}
  447 + results << %Q{<option value="Virgin Islands (British)">Virgin Islands (British)</option>}
  448 + results << %Q{<option value="Virgin Islands (U.S.)">Virgin Islands (U.S.)</option>}
  449 + results << %Q{<option value="Western Sahara">Western Sahara</option>}
  450 + results << %Q{<option value="Yemen">Yemen</option>}
  451 + results << %Q{<option value="Zambia">Zambia</option>}
  452 + end
  453 +
  454 + protected
  455 +
  456 + def input_tag_html(type, opts=tag_attr)
  457 + options = { :id => tag_attr[:name], :value => "" }.update(opts)
  458 + results = %Q(<input type="#{type}" )
  459 + results << %Q(name="content[#{options[:name]}]" ) if tag_attr[:name]
  460 + results << %Q(#{add_attrs_to("", options)}/>)
  461 + end
  462 +
  463 + def add_attrs_to(results, tag_attrs=tag_attr)
  464 + attrs_to_add = tag_attrs.stringify_keys
  465 + attrs_to_add['class'] = (attrs_to_add['class'].to_s + " #{attrs_to_add.delete('validate')}") if attrs_to_add['validate']
  466 +
  467 + attrs_to_add.sort.each do |name, value|
  468 + results << %Q(#{name.to_s}="#{value.to_s}" ) unless name == 'name'
  469 + end
  470 + results
  471 + end
  472 +
  473 + def raise_name_error(tag_name)
  474 + raise(DatabaseFormTagError.new("`#{tag_name}' tag requires a `name' attribute"))
  475 + end
  476 +
  477 + def raise_error_if_name_missing(tag_name)
  478 + raise_name_error(tag_name) if tag_attr[:name].nil? or tag_attr[:name].empty?
  479 + end
  480 +end
16 app/models/form_response.rb
... ... @@ -0,0 +1,16 @@
  1 +class FormResponse < ActiveRecord::Base
  2 + validates_presence_of :name, :content
  3 + serialize :content, Hash
  4 +
  5 + def to_xml(options = {})
  6 + options[:indent] ||= 2
  7 + xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
  8 + xml.instruct! unless options[:skip_instruct]
  9 + xml.tag!("form-response", :id => id) do
  10 + xml.tag!("created-at", created_at.strftime("%Y-%m-%d %H:%M"))
  11 + content.each do |name, value|
  12 + xml.tag!(name.dasherize, value)
  13 + end
  14 + end
  15 + end
  16 +end
23 app/views/admin/form_responses/index.rhtml
... ... @@ -0,0 +1,23 @@
  1 +<h1>Export Form Responses</h1>
  2 +
  3 +<p>Select a form name and a date range. Then choose 'Export' to generate an XML file.</p>
  4 +
  5 +<% form_tag(:action => 'export') do -%>
  6 + <table cellpadding="5">
  7 + <tr>
  8 + <td>Form Name:</td>
  9 + <td><%= select(:filter, :name, @form_names) %></td>
  10 + </tr>
  11 + <tr>
  12 + <td>Start Date:</td>
  13 + <td><%= datetime_select(:filter, :start_time) %></td>
  14 + </tr>
  15 + <tr>
  16 + <td>End Date:</td>
  17 + <td><%= datetime_select(:filter, :end_time) %></td>
  18 + </tr>
  19 + <tr>
  20 + <td colspan="2"><%= submit_tag('Export') %></td>
  21 + </tr>
  22 + </table>
  23 +<% end -%>
18 database_form_extension.rb
... ... @@ -0,0 +1,18 @@
  1 +class DatabaseFormExtension < Radiant::Extension
  2 + version "0.2"
  3 + description "Provides a page type for email contact and request forms and saves requests to a database (modified Mailer)."
  4 + url "http://ubikorp.com/projects"
  5 +
  6 + define_routes do |map|
  7 + map.connect 'admin/form_responses/:action/:id', :controller => 'admin/form_responses'
  8 + end
  9 +
  10 + def activate
  11 + admin.tabs.add "Form Responses", "/admin/form_responses", :before => "Layouts"
  12 + DatabaseFormPage
  13 + end
  14 +
  15 + def deactivate
  16 + admin.tabs.remove "Form Responses"
  17 + end
  18 +end
13 db/migrate/001_create_info_requests.rb
... ... @@ -0,0 +1,13 @@
  1 +class CreateInfoRequests < ActiveRecord::Migration
  2 + def self.up
  3 + create_table :form_responses do |t|
  4 + t.column :name, :string
  5 + t.column :content, :text
  6 + t.column :created_at, :datetime
  7 + end
  8 + end
  9 +
  10 + def self.down
  11 + drop_table :form_responses
  12 + end
  13 +end
21 lib/tasks/database_form_extension_tasks.rake
... ... @@ -0,0 +1,21 @@
  1 +namespace :radiant do
  2 + namespace :extensions do
  3 + namespace :database_form do
  4 +
  5 + desc "Runs the migration of the Database Form extension"
  6 + task :migrate => :environment do
  7 + require 'radiant/extension_migrator'
  8 + if ENV["VERSION"]
  9 + DatabaseFormExtension.migrator.migrate(ENV["VERSION"].to_i)
  10 + else
  11 + DatabaseFormExtension.migrator.migrate
  12 + end
  13 + end
  14 +
  15 + desc "Copies the Database Form extension assets to the public directory"
  16 + task :update => :environment do
  17 + FileUtils.cp DatabaseFormExtension.root + "/public/javascripts/validation.js", RAILS_ROOT + "/public/javascripts"
  18 + end
  19 + end
  20 + end
  21 +end
280 public/javascripts/validation.js
... ... @@ -0,0 +1,280 @@
  1 +/*
  2 +* Really easy field validation with Prototype
  3 +* http://tetlaw.id.au/view/javascript/really-easy-field-validation
  4 +* Andrew Tetlaw
  5 +* Version 1.5.4.1 (2007-01-05)
  6 +*
  7 +* Copyright (c) 2007 Andrew Tetlaw
  8 +* Permission is hereby granted, free of charge, to any person
  9 +* obtaining a copy of this software and associated documentation
  10 +* files (the "Software"), to deal in the Software without
  11 +* restriction, including without limitation the rights to use, copy,
  12 +* modify, merge, publish, distribute, sublicense, and/or sell copies
  13 +* of the Software, and to permit persons to whom the Software is
  14 +* furnished to do so, subject to the following conditions:
  15 +*
  16 +* The above copyright notice and this permission notice shall be
  17 +* included in all copies or substantial portions of the Software.
  18 +*
  19 +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  20 +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  21 +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  22 +* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
  23 +* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
  24 +* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  25 +* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  26 +* SOFTWARE.
  27 +*
  28 +*/
  29 +var Validator = Class.create();
  30 +
  31 +Validator.prototype = {
  32 + initialize : function(className, error, test, options) {
  33 + if(typeof test == 'function'){
  34 + this.options = $H(options);
  35 + this._test = test;
  36 + } else {
  37 + this.options = $H(test);
  38 + this._test = function(){return true};
  39 + }
  40 + this.error = error || 'Validation failed.';
  41 + this.className = className;
  42 + },
  43 + test : function(v, elm) {
  44 + return (this._test(v,elm) && this.options.all(function(p){
  45 + return Validator.methods[p.key] ? Validator.methods[p.key](v,elm,p.value) : true;
  46 + }));
  47 + }
  48 +}
  49 +Validator.methods = {
  50 + pattern : function(v,elm,opt) {return Validation.get('IsEmpty').test(v) || opt.test(v)},
  51 + minLength : function(v,elm,opt) {return v.length >= opt},
  52 + maxLength : function(v,elm,opt) {return v.length <= opt},
  53 + min : function(v,elm,opt) {return v >= parseFloat(opt)},
  54 + max : function(v,elm,opt) {return v <= parseFloat(opt)},
  55 + notOneOf : function(v,elm,opt) {return $A(opt).all(function(value) {
  56 + return v != value;
  57 + })},
  58 + oneOf : function(v,elm,opt) {return $A(opt).any(function(value) {
  59 + return v == value;
  60 + })},
  61 + is : function(v,elm,opt) {return v == opt},
  62 + isNot : function(v,elm,opt) {return v != opt},
  63 + equalToField : function(v,elm,opt) {return v == $F(opt)},
  64 + notEqualToField : function(v,elm,opt) {return v != $F(opt)},
  65 + include : function(v,elm,opt) {return $A(opt).all(function(value) {
  66 + return Validation.get(value).test(v,elm);
  67 + })}
  68 +}
  69 +
  70 +var Validation = Class.create();
  71 +
  72 +Validation.prototype = {
  73 + initialize : function(form, options){
  74 + this.options = Object.extend({
  75 + onSubmit : true,
  76 + stopOnFirst : false,
  77 + immediate : false,
  78 + focusOnError : true,
  79 + useTitles : false,
  80 + onFormValidate : function(result, form) {},
  81 + onElementValidate : function(result, elm) {}
  82 + }, options || {});
  83 + this.form = $(form);
  84 + if(this.options.onSubmit) Event.observe(this.form,'submit',this.onSubmit.bind(this),false);
  85 + if(this.options.immediate) {
  86 + var useTitles = this.options.useTitles;
  87 + var callback = this.options.onElementValidate;
  88 + Form.getElements(this.form).each(function(input) { // Thanks Mike!
  89 + Event.observe(input, 'blur', function(ev) { Validation.validate(Event.element(ev),{useTitle : useTitles, onElementValidate : callback}); });
  90 + });
  91 + }
  92 + },
  93 + onSubmit : function(ev){
  94 + if(!this.validate()) Event.stop(ev);
  95 + },
  96 + validate : function() {
  97 + var result = false;
  98 + var useTitles = this.options.useTitles;
  99 + var callback = this.options.onElementValidate;
  100 + if(this.options.stopOnFirst) {
  101 + result = Form.getElements(this.form).all(function(elm) { return Validation.validate(elm,{useTitle : useTitles, onElementValidate : callback}); });
  102 + } else {
  103 + result = Form.getElements(this.form).collect(function(elm) { return Validation.validate(elm,{useTitle : useTitles, onElementValidate : callback}); }).all();
  104 + }
  105 + if(!result && this.options.focusOnError) {
  106 + Form.getElements(this.form).findAll(function(elm){return $(elm).hasClassName('validation-failed')}).first().focus()
  107 + }
  108 + this.options.onFormValidate(result, this.form);
  109 + return result;
  110 + },
  111 + reset : function() {
  112 + Form.getElements(this.form).each(Validation.reset);
  113 + }
  114 +}
  115 +
  116 +Object.extend(Validation, {
  117 + validate : function(elm, options){
  118 + options = Object.extend({
  119 + useTitle : false,
  120 + onElementValidate : function(result, elm) {}
  121 + }, options || {});
  122 + elm = $(elm);
  123 + var cn = elm.classNames();
  124 + return result = cn.all(function(value) {
  125 + var test = Validation.test(value,elm,options.useTitle);
  126 + options.onElementValidate(test, elm);
  127 + return test;
  128 + });
  129 + },
  130 + test : function(name, elm, useTitle) {
  131 + var v = Validation.get(name);
  132 + var prop = '__advice'+name.camelize();
  133 + try {
  134 + if(Validation.isVisible(elm) && !v.test($F(elm), elm)) {
  135 + if(!elm[prop]) {
  136 + var advice = Validation.getAdvice(name, elm);
  137 + if(advice == null) {
  138 + var errorMsg = useTitle ? ((elm && elm.title) ? elm.title : v.error) : v.error;
  139 + advice = '<div class="validation-advice" id="advice-' + name + '-' + Validation.getElmID(elm) +'" style="display:none">' + errorMsg + '</div>'
  140 + switch (elm.type.toLowerCase()) {
  141 + case 'checkbox':
  142 + case 'radio':
  143 + var p = elm.parentNode;
  144 + if(p) {
  145 + new Insertion.Bottom(p, advice);
  146 + } else {
  147 + new Insertion.After(elm, advice);
  148 + }
  149 + break;
  150 + default:
  151 + new Insertion.After(elm, advice);
  152 + }
  153 + advice = Validation.getAdvice(name, elm);
  154 + }
  155 + if(typeof Effect == 'undefined') {
  156 + advice.style.display = 'block';
  157 + } else {
  158 + new Effect.Appear(advice, {duration : 1 });
  159 + }
  160 + }
  161 + elm[prop] = true;
  162 + elm.removeClassName('validation-passed');
  163 + elm.addClassName('validation-failed');
  164 + return false;
  165 + } else {
  166 + var advice = Validation.getAdvice(name, elm);
  167 + if(advice != null) advice.hide();
  168 + elm[prop] = '';
  169 + elm.removeClassName('validation-failed');
  170 + elm.addClassName('validation-passed');
  171 + return true;
  172 + }
  173 + } catch(e) {
  174 + throw(e)
  175 + }
  176 + },
  177 + isVisible : function(elm) {
  178 + while(elm.tagName != 'BODY') {
  179 + if(!$(elm).visible()) return false;
  180 + elm = elm.parentNode;
  181 + }
  182 + return true;
  183 + },
  184 + getAdvice : function(name, elm) {
  185 + return $('advice-' + name + '-' + Validation.getElmID(elm)) || $('advice-' + Validation.getElmID(elm));
  186 + },
  187 + getElmID : function(elm) {
  188 + return elm.id ? elm.id : elm.name;
  189 + },
  190 + reset : function(elm) {
  191 + elm = $(elm);
  192 + var cn = elm.classNames();
  193 + cn.each(function(value) {
  194 + var prop = '__advice'+value.camelize();
  195 + if(elm[prop]) {
  196 + var advice = Validation.getAdvice(value, elm);
  197 + advice.hide();
  198 + elm[prop] = '';
  199 + }
  200 + elm.removeClassName('validation-failed');
  201 + elm.removeClassName('validation-passed');
  202 + });
  203 + },
  204 + add : function(className, error, test, options) {
  205 + var nv = {};
  206 + nv[className] = new Validator(className, error, test, options);
  207 + Object.extend(Validation.methods, nv);
  208 + },
  209 + addAllThese : function(validators) {
  210 + var nv = {};
  211 + $A(validators).each(function(value) {
  212 + nv[value[0]] = new Validator(value[0], value[1], value[2], (value.length > 3 ? value[3] : {}));
  213 + });
  214 + Object.extend(Validation.methods, nv);
  215 + },
  216 + get : function(name) {
  217 + return Validation.methods[name] ? Validation.methods[name] : Validation.methods['_LikeNoIDIEverSaw_'];
  218 + },
  219 + methods : {
  220 + '_LikeNoIDIEverSaw_' : new Validator('_LikeNoIDIEverSaw_','',{})
  221 + }
  222 +});
  223 +
  224 +Validation.add('IsEmpty', '', function(v) {
  225 + return ((v == null) || (v.length == 0)); // || /^\s+$/.test(v));
  226 + });
  227 +
  228 +Validation.addAllThese([
  229 + ['required', 'This is a required field.', function(v) {
  230 + return !Validation.get('IsEmpty').test(v);
  231 + }],
  232 + ['validate-number', 'Please enter a valid number in this field.', function(v) {
  233 + return Validation.get('IsEmpty').test(v) || (!isNaN(v) && !/^\s+$/.test(v));
  234 + }],
  235 + ['validate-digits', 'Please use numbers only in this field. please avoid spaces or other characters such as dots or commas.', function(v) {
  236 + return Validation.get('IsEmpty').test(v) || !/[^\d]/.test(v);
  237 + }],
  238 + ['validate-alpha', 'Please use letters only (a-z) in this field.', function (v) {
  239 + return Validation.get('IsEmpty').test(v) || /^[a-zA-Z]+$/.test(v)
  240 + }],
  241 + ['validate-alphanum', 'Please use only letters (a-z) or numbers (0-9) only in this field. No spaces or other characters are allowed.', function(v) {
  242 + return Validation.get('IsEmpty').test(v) || !/\W/.test(v)
  243 + }],
  244 + ['validate-date', 'Please enter a valid date.', function(v) {
  245 + var test = new Date(v);
  246 + return Validation.get('IsEmpty').test(v) || !isNaN(test);
  247 + }],
  248 + ['validate-email', 'Please enter a valid email address. For example fred@domain.com .', function (v) {
  249 + return Validation.get('IsEmpty').test(v) || /\w{1,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/.test(v)
  250 + }],
  251 + ['validate-url', 'Please enter a valid URL.', function (v) {
  252 + return Validation.get('IsEmpty').test(v) || /^(http|https|ftp):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i.test(v)
  253 + }],
  254 + ['validate-date-au', 'Please use this date format: dd/mm/yyyy. For example 17/03/2006 for the 17th of March, 2006.', function(v) {
  255 + if(Validation.get('IsEmpty').test(v)) return true;
  256 + var regex = /^(\d{2})\/(\d{2})\/(\d{4})$/;
  257 + if(!regex.test(v)) return false;
  258 + var d = new Date(v.replace(regex, '$2/$1/$3'));
  259 + return ( parseInt(RegExp.$2, 10) == (1+d.getMonth()) ) &&
  260 + (parseInt(RegExp.$1, 10) == d.getDate()) &&
  261 + (parseInt(RegExp.$3, 10) == d.getFullYear() );
  262 + }],
  263 + ['validate-currency-dollar', 'Please enter a valid $ amount. For example $100.00 .', function(v) {
  264 + // [$]1[##][,###]+[.##]
  265 + // [$]1###+[.##]
  266 + // [$]0.##
  267 + // [$].##
  268 + return Validation.get('IsEmpty').test(v) || /^\$?\-?([1-9]{1}[0-9]{0,2}(\,[0-9]{3})*(\.[0-9]{0,2})?|[1-9]{1}\d*(\.[0-9]{0,2})?|0(\.[0-9]{0,2})?|(\.[0-9]{1,2})?)$/.test(v)
  269 + }],
  270 + ['validate-selection', 'Please make a selection', function(v,elm){
  271 + return elm.options ? elm.selectedIndex > 0 : !Validation.get('IsEmpty').test(v);
  272 + }],
  273 + ['validate-one-required', 'Please select one of the above options.', function (v,elm) {
  274 + var p = elm.parentNode;
  275 + var options = p.getElementsByTagName('INPUT');
  276 + return $A(options).any(function(elm) {
  277 + return $F(elm);
  278 + });
  279 + }]
  280 +]);
20 test/fixtures/form_responses.yml
... ... @@ -0,0 +1,20 @@
  1 +first:
  2 + name: contact
  3 + content:
  4 + email: 'test1@test.net'
  5 + name: 'nap'
  6 + created_at: <%= 3.days.ago.to_s :db %>
  7 +
  8 +second:
  9 + name: contact
  10 + content:
  11 + email: 'test2@test.net'
  12 + name: 'ian'
  13 + created_at: <%= 2.days.ago.to_s :db %>
  14 +
  15 +third:
  16 + name: requestinfo
  17 + content:
  18 + email: 'info@request.net'
  19 + name: 'inforequest'
  20 + created_at: <%= 2.days.ago.to_s :db %>
17 test/fixtures/pages.yml
... ... @@ -0,0 +1,17 @@
  1 +homepage:
  2 + id: 1
  3 + title: Ruby Home Page
  4 + breadcrumb: Home
  5 + slug: /
  6 + status_id: 100
  7 + parent_id:
  8 + published_at: 2006-01-30 08:41:07
  9 +contact_form:
  10 + id: 2
  11 + title: Contact Form
  12 + breadcrumb: Contact
  13 + slug: contact
  14 + status_id: 100
  15 + parent_id: 1
  16 + class_name: DatabaseFormPage
  17 + published_at: 2006-02-05 08:44:07
39 test/functional/admin/form_responses_controller_test.rb
... ... @@ -0,0 +1,39 @@
  1 +require File.dirname(__FILE__) + '/../../test_helper'
  2 +
  3 +# Re-raise errors caught by the controller.
  4 +Admin::FormResponsesController.class_eval { def rescue_action(e) raise e end }
  5 +
  6 +class Admin::FormResponsesControllerTest < Test::Unit::TestCase
  7 + fixtures :form_responses
  8 + test_helper :login
  9 +
  10 + def setup
  11 + @controller = Admin::FormResponsesController.new
  12 + @request = ActionController::TestRequest.new
  13 + @response = ActionController::TestResponse.new
  14 + login_as :existing
  15 + end
  16 +
  17 + def test_index
  18 + get :index
  19 + assert_response :success
  20 + assert_template 'index'
  21 + assert_not_nil assigns(:filter)
  22 + assert_not_nil assigns(:form_names)
  23 + end
  24 +
  25 + def test_export
  26 + post :export
  27 + assert_response :success
  28 + assert_match 'application/xml', @response.headers['Content-Type']
  29 + assert_select 'form-responses'
  30 + end
  31 +
  32 + def test_export_filter
  33 + post :export, :filter => { :name => 'contact' }
  34 + assert_response :success
  35 + assert_match '<name>nap</name', @response.body
  36 + assert_select 'name', :text => 'ian'
  37 + assert_select 'name', :text => 'inforequest', :count => 0
  38 + end
  39 +end
12 test/functional/database_form_extension_test.rb
... ... @@ -0,0 +1,12 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class DatabaseFormExtensionTest < Test::Unit::TestCase
  4 + def test_initialization
  5 + assert_equal File.join(File.expand_path(RAILS_ROOT), 'vendor', 'extensions', 'database_form'), DatabaseFormExtension.root
  6 + assert_equal 'Database Form', DatabaseFormExtension.extension_name
  7 + end
  8 +
  9 + def test_should_define_pages
  10 + assert defined?(DatabaseFormPage)
  11 + end
  12 +end
32 test/functional/database_form_page_test.rb
... ... @@ -0,0 +1,32 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class DatabaseFormPageTest < Test::Unit::TestCase
  4 + fixtures :pages, :form_responses
  5 + test_helper :login, :pages, :difference
  6 +