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 |