implement feature #208

did committed Aug 13, 2014
commit 47a10d1afc7e875bc9eb34c64d4ee4b5c9c93800
Showing 5 changed files with 227 additions and 37 deletions
locomotive/wagon/liquid/tags/model_form.rb b/lib/locomotive/wagon/liquid/tags/model_form.rb +67 -0
@@ @@ -0,0 +1,67 @@
+ module Locomotive
+ module Wagon
+ module Liquid
+ module Tags
+
+ # Display the form html tag with the appropriate hidden fields in order to create
+ # a content entry from a public site.
+ # It handles callbacks, csrf and target url out of the box.
+ #
+ # Usage:
+ #
+ # {% model_form 'newsletter_addresses' %}
+ # <input type='text' name='content[email]' />
+ # <input type='submit' value='Add' />
+ # {% endform_form %}
+ #
+ # {% model_form 'newsletter_addresses', class: 'a-css-class', success: 'http://www.google.fr', error: '/error' %}...{% endform_form %}
+ #
+ class ModelForm < Solid::Block
+
+ tag_name :model_form
+
+ def display(*options, &block)
+ name = options.shift
+ options = options.shift || {}
+
+ form_attributes = { method: 'POST', enctype: 'multipart/form-data' }.merge(options.slice(:id, :class))
+
+ html_content_tag :form,
+ content_type_html(name) + callbacks_html(options) + yield,
+ form_attributes
+ end
+
+ def content_type_html(name)
+ html_tag :input, type: 'hidden', name: 'content_type_slug', value: name
+ end
+
+ def callbacks_html(options)
+ options.slice(:success, :error).map do |(name, value)|
+ html_tag :input, type: 'hidden', name: "#{name}_callback", value: value
+ end.join('')
+ end
+
+ private
+
+ def html_content_tag(name, content, options = {})
+ "<#{name} #{inline_options(options)}>#{content}</#{name}>"
+ end
+
+ def html_tag(name, options = {})
+ "<#{name} #{inline_options(options)} />"
+ end
+
+ # Write options (Hash) into a string according to the following pattern:
+ # <key1>="<value1>", <key2>="<value2", ...etc
+ def inline_options(options = {})
+ return '' if options.empty?
+ (options.stringify_keys.to_a.collect { |a, b| "#{a}=\"#{b}\"" }).join(' ')
+ end
+
+ end
+
+ end
+
+ end
+ end
+ end
\ No newline at end of file
locomotive/wagon/server/entry_submission.rb b/lib/locomotive/wagon/server/entry_submission.rb +68 -35
@@ @@ -8,31 +8,10 @@ module Locomotive::Wagon
def call(env)
self.set_accessors(env)
- if self.request.post? && env['PATH_INFO'] =~ /^\/entry_submissions\/(.*)/
- self.process_form($1)
+ if slug = get_content_type_slug(env)
+ self.process_form(slug)
- # puts "html? #{html?} / json? #{json?} / #{self.callback_url} / #{params.inspect}"
-
- if @entry.valid?
- if self.html?
- self.record_submitted_entry
- self.redirect_to self.callback_url
- elsif self.json?
- self.json_response
- end
- else
- if self.html?
- if self.callback_url =~ /^http:\/\//
- self.redirect_to self.callback_url
- else
- env['PATH_INFO'] = self.callback_url
- self.liquid_assigns[@content_type.slug.singularize] = @entry
- app.call(env)
- end
- elsif self.json?
- self.json_response(422)
- end
- end
+ self.navigation_behavior(env)
else
self.fetch_submitted_entry
@@ @@ -40,15 +19,73 @@ module Locomotive::Wagon
end
end
+ # Render or redirect depending on:
+ # - the status of the content entry (valid or not)
+ # - the presence of a callback or not
+ # - the type of response asked by the browser (html or json)
+ #
+ def navigation_behavior(env)
+ if @entry.valid?
+ navigation_success(env)
+ else
+ navigation_error(env)
+ end
+ end
+
+ def navigation_success(env)
+ if self.html?
+ self.record_submitted_entry
+ self.redirect_to success_location
+ elsif self.json?
+ self.json_response
+ end
+ end
+
+ def navigation_error(env)
+ if self.html?
+ if error_location =~ %r(^http://)
+ self.redirect_to error_location
+ else
+ env['PATH_INFO'] = error_location
+ self.liquid_assigns[@content_type.slug.singularize] = @entry
+ app.call(env)
+ end
+ elsif self.json?
+ self.json_response(422)
+ end
+ end
+
protected
+ def success_location; location(:success); end
+ def error_location; location(:error); end
+
+ def location(state)
+ params[:"#{state}_callback"] || (entry_submissions_path? ? '/' : path_info)
+ end
+
+ def entry_submissions_path?
+ !(path_info =~ %r(^/entry_submissions/)).nil?
+ end
+
+ # Get the slug (or permalink) of the content type either from the PATH_INFO variable (old way)
+ # or from the presence of the content_type_slug param (model_form tag).
+ #
+ def get_content_type_slug(env)
+ if request.post? && (path_info =~ %r(^/entry_submissions/(.*)) || params[:content_type_slug])
+ $1 || params[:content_type_slug]
+ end
+ end
+
+ # Record in session the newly "persisted" content entry.
+ #
def record_submitted_entry
- self.request.session[:now] ||= {}
- self.request.session[:now][:submitted_entry] = [@content_type.slug, @entry._slug]
+ session[:now] ||= {}
+ session[:now][:submitted_entry] = [@content_type.slug, @entry._slug]
end
def fetch_submitted_entry
- if data = self.request.session[:now].try(:delete, :submitted_entry)
+ if data = session[:now].try(:delete, :submitted_entry)
content_type = self.mounting_point.content_types[data.first.to_s]
entry = (content_type.entries || []).detect { |e| e._slug == data.last }
@@ @@ -65,13 +102,13 @@ module Locomotive::Wagon
# Mimic the creation of a content entry with a minimal validation.
#
- # @param [ String ] permalink The permalink (or slug) of the content type
+ # @param [ String ] slug The slug (or permalink) of the content type
#
#
- def process_form(permalink)
- permalink = permalink.split('.').first
+ def process_form(slug)
+ slug = slug.split('.').first
- @content_type = self.mounting_point.content_types[permalink]
+ @content_type = self.mounting_point.content_types[slug]
raise "Unknown content type '#{@content_type.inspect}'" if @content_type.nil?
@@ @@ -83,10 +120,6 @@ module Locomotive::Wagon
@content_type.entries.delete(@entry) if !@entry.valid?
end
- def callback_url
- (@entry.valid? ? params[:success_callback] : params[:error_callback]) || '/'
- end
-
# Build the JSON response
#
# @param [ Integer ] status The HTTP return code
locomotive/wagon/server/middleware.rb b/lib/locomotive/wagon/server/middleware.rb +4 -2
@@ @@ -3,8 +3,10 @@ module Locomotive::Wagon
class Middleware
- attr_accessor :app, :request, :path, :liquid_assigns
+ extend Forwardable
+ def_delegators :request, :path_info, :session
+ attr_accessor :app, :request, :path, :liquid_assigns
attr_accessor :mounting_point, :page, :content_entry
def initialize(app = nil)
@@ @@ -18,8 +20,8 @@ module Locomotive::Wagon
protected
def set_accessors(env)
- self.path = env['wagon.path']
self.request = Rack::Request.new(env)
+ self.path = env['wagon.path']
self.mounting_point = env['wagon.mounting_point']
self.page = env['wagon.page']
self.content_entry = env['wagon.content_entry']
spec/fixtures/default/app/views/pages/events.liquid.haml +21 -0
@@ @@ -19,6 +19,27 @@ position: 5
{% endfor %}
#sidebar.unit.size1of3
+
+ {% model_form 'messages', id: 'contactform' %}
+ %p
+ %label{ :for => 'name' } Name
+ %input{ :type => 'text', :id => 'name', :name => 'content[name]', :placeholder => 'First and last name', :tabindex => '1', required: 'required', value: '{{ message.name }}' }
+ %span {{ message.errors.name }}
+
+ %p
+ %label{ :for => 'email' } Email
+ %input{ :type => 'text', :id => 'email', :name => 'content[email]', :placeholder => 'example@domain.com', :tabindex => '2', required: 'required', value: '{{ message.email }}' }
+ %span {{ message.errors.email }}
+
+ %p
+ %label{ :for => 'comment' } Your Message
+ %textarea{ :id => 'comment', :name => 'content[message]', :tabindex => '3', required: 'required' } {{ message.message }}
+ %span {{ message.errors.message }}
+
+ %p.action
+ %input{ :name => 'submit', :type => 'submit', :tabindex => '4', :value => 'Send Message' }
+ {% endmodel_form %}
+
{% editable_long_text 'sidebar' %}
%p
spec/integration/server/new_contact_form_spec.rb +67 -0
@@ @@ -0,0 +1,67 @@
+ # encoding: utf-8
+
+ require File.dirname(__FILE__) + '/../integration_helper'
+ require 'locomotive/wagon/server'
+ require 'rack/test'
+
+ describe 'NewContactForm' do
+
+ include Rack::Test::Methods
+
+ def app
+ run_server
+ end
+
+ it 'renders the form' do
+ get '/events'
+ last_response.body.should =~ %r(<form method="POST" enctype="multipart/form-data" id="contactform">)
+ end
+
+ describe '#submit' do
+
+ let(:params) { {
+ 'content_type_slug' => 'messages',
+ 'entry' => { 'name' => 'John', 'email' => 'j@doe.net', 'message' => 'Bla bla' } } }
+ let(:response) { post_contact_form(params) }
+ let(:status) { response.status }
+
+ context 'when not valid' do
+
+ let(:params) { { 'content_type_slug' => 'messages' } }
+
+ it 'returns a success status' do
+ response.status.should == 200
+ end
+
+ it 'displays errors' do
+ response.body.to_s.should =~ /can't not be blank/
+ end
+
+ end
+
+ context 'when valid' do
+
+ let(:response) { post_contact_form(params, true) }
+
+ it 'returns a success status' do
+ response.status.should == 200
+ end
+
+ it 'displays a success message' do
+ response.body.should =~ /Thank you John/
+ end
+
+ end
+
+ end
+
+ def post_contact_form(params, follow_redirect = false)
+ url = '/events'
+ post url, params
+
+ follow_redirect! if follow_redirect
+
+ last_response
+ end
+
+ end