implement the service to validate and persist content entries + refactor the entry_submission middleware (still missing specs)
did
committed Feb 17, 2015
commit 841d607a9a1af0bc4f9f87f57d0a9c22ae37d679
Showing 29
changed files with
676 additions
and 351 deletions
config/locales/en.yml
+2
-1
| @@ | @@ -27,6 +27,7 @@ en: |
| session_assign: "Syntax Error in 'session_assign' - Valid syntax: session_assign [var] = [source]" | |
| messages: | |
| blank: "can't not be blank" | |
| + | unique: "must be unique" |
| pagination: | |
| previous: "« Previous" | |
| @@ | @@ -186,4 +187,4 @@ en: |
| default: ! '%a, %d %b %Y %H:%M:%S %z' | |
| long: ! '%B %d, %Y %H:%M' | |
| short: ! '%d %b %H:%M' | |
| - | pm: pm |
| \ No newline at end of file | |
| + | pm: pm |
config/locales/fr.yml
+2
-1
| @@ | @@ -17,6 +17,7 @@ fr: |
| errors: | |
| messages: | |
| blank: "doit être rempli(e)" | |
| + | unique: "doit être unique" |
| pagination: | |
| previous: "« Précédent" | |
| @@ | @@ -145,4 +146,4 @@ fr: |
| skip_last_comma: true | |
| words_connector: ", " | |
| two_words_connector: " et " | |
| - | last_word_connector: " et " |
| \ No newline at end of file | |
| + | last_word_connector: " et " |
locomotive/steam/middlewares.rb b/lib/locomotive/steam/middlewares.rb
+1
-1
| @@ | @@ -8,7 +8,7 @@ require_relative 'middlewares/locale' |
| require_relative 'middlewares/timezone' | |
| require_relative 'middlewares/logging' | |
| require_relative 'middlewares/path' | |
| - | # require_relative 'middlewares/entry_submission' |
| + | require_relative 'middlewares/entry_submission' |
| require_relative 'middlewares/page' | |
| require_relative 'middlewares/templatized_page' | |
locomotive/steam/middlewares/base.rb b/lib/locomotive/steam/middlewares/base.rb
+0
-71
| @@ | @@ -1,71 +0,0 @@ |
| - | # module Locomotive::Steam |
| - | # module Middlewares |
| - | |
| - | # class Base |
| - | |
| - | # # attr_accessor :app, :request, :path |
| - | # # attr_accessor :liquid_assigns, :services |
| - | # # attr_accessor :site, :page, :content_entry, :locale |
| - | |
| - | # # def initialize(app = nil) |
| - | # # puts "...creating #{self.class.name}... #{app.nil?}" |
| - | # # @app = app |
| - | # # end |
| - | |
| - | # # def call(env) |
| - | # # dup._call(env) # thread-safe purpose |
| - | # # # _call(env) |
| - | # # # if Locomotive::Steam.configuration.mode == :test |
| - | # # # _call(env) |
| - | # # # else |
| - | # # # end |
| - | # # end |
| - | |
| - | # # def _call(env) |
| - | # # code, headers, response = @app.call(env) |
| - | # # # self.set_accessors(env) |
| - | # # [code, headers, [response]] |
| - | # # end |
| - | |
| - | # # protected |
| - | |
| - | # # # def path |
| - | # # # @path ||= @env.fetch('steam.path', nil) |
| - | # # # end |
| - | |
| - | # # # def set_accessors(env) |
| - | # # # %w(path site request page content_entry services locale).each do |name| |
| - | # # # self.send(:"#{name}=", env.fetch("steam.#{name}", nil)) |
| - | # # # end |
| - | |
| - | # # # env['steam.liquid_assigns'] ||= {} |
| - | # # # self.liquid_assigns = env.fetch('steam.liquid_assigns') |
| - | # # # end |
| - | |
| - | # # def params |
| - | # # self.request.params.deep_symbolize_keys |
| - | # # end |
| - | |
| - | # # def html? |
| - | # # ['text/html', 'application/x-www-form-urlencoded'].include?(self.request.media_type) && |
| - | # # !self.request.xhr? && |
| - | # # !self.json? |
| - | # # end |
| - | |
| - | # # def json? |
| - | # # self.request.content_type == 'application/json' || File.extname(self.request.path) == '.json' |
| - | # # end |
| - | |
| - | # # def redirect_to(location, type = 301) |
| - | # # self.log "Redirected to #{location}" |
| - | # # [type, { 'Content-Type' => 'text/html', 'Location' => location }, []] |
| - | # # end |
| - | |
| - | # # def log(msg) |
| - | # # Locomotive::Common::Logger.info msg |
| - | # # end |
| - | |
| - | # end |
| - | |
| - | # end |
| - | # end |
locomotive/steam/middlewares/entry_submission.rb b/lib/locomotive/steam/middlewares/entry_submission.rb
+115
-79
| @@ | @@ -1,120 +1,156 @@ |
| module Locomotive::Steam | |
| module Middlewares | |
| - | # Mimic the submission of a content entry |
| + | # Submit a content entry and persist it |
| # | |
| - | class EntrySubmission < Base |
| - | |
| - | def _call(env) |
| - | super |
| - | |
| - | if self.request.post? && env['PATH_INFO'] =~ /^\/entry_submissions\/(.*)/ |
| - | self.process_form($1) |
| - | |
| - | # 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 |
| + | class EntrySubmission < ThreadSafe |
| + | |
| + | include Helpers |
| + | |
| + | HTTP_REGEXP = /^https?:\/\//o |
| + | ENTRY_SUBMISSION_REGEXP = /^\/entry_submissions\/(\w+)/o |
| + | SUBMITTED_TYPE_PARAM = 'submitted_type_slug' |
| + | SUBMITTED_PARAM = 'submitted_entry_slug' |
| + | |
| + | def _call |
| + | if slug = get_content_type_slug |
| + | # we didn't go through the locale middleware yet, |
| + | # so set the locale manually. Needed to build a localized |
| + | # version of the entry + error messages (if present). |
| + | with_locale do |
| + | entry = create_entry(slug) |
| + | navigation_behavior(entry) |
| end | |
| else | |
| - | self.fetch_submitted_entry |
| + | fetch_entry |
| + | end |
| + | end |
| - | app.call(env) |
| + | # 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(entry) |
| + | if entry.errors.empty? |
| + | navigation_success(entry) |
| + | else |
| + | navigation_error(entry) |
| end | |
| end | |
| - | protected |
| + | def navigation_success(entry) |
| + | if html? |
| + | redirect_to success_location(entry_to_query_string(query)) |
| + | elsif json? |
| + | json_response(entry) |
| + | end |
| + | end |
| - | def record_submitted_entry |
| - | self.request.session[:now] ||= {} |
| - | self.request.session[:now][:submitted_entry] = [@content_type.slug, @entry._slug] |
| + | def navigation_error(entry) |
| + | if html? |
| + | navigation_html_error(entry) |
| + | elsif json? |
| + | json_response(entry, 422) |
| + | end |
| end | |
| - | def fetch_submitted_entry |
| - | if data = self.request.session[:now].try(:delete, :submitted_entry) |
| - | content_type = self.mounting_point.content_types[data.first.to_s] |
| + | def navigation_html_error(entry) |
| + | if error_location =~ HTTP_REGEXP |
| + | redirect_to error_location |
| + | else |
| + | env['PATH_INFO'] = error_location |
| + | store_in_liquid(entry) |
| + | self.next |
| + | end |
| + | end |
| - | entry = (content_type.entries || []).detect { |e| e._slug == data.last } |
| + | private |
| - | # do not keep track of the entry |
| - | content_type.entries.delete(entry) if entry |
| + | def store_in_liquid(entry) |
| + | liquid_assigns[entry.content_type_slug.singularize] = entry |
| + | end |
| - | # add it to the additional liquid assigns for the next liquid rendering |
| - | if entry |
| - | self.liquid_assigns[content_type.slug.singularize] = entry |
| - | end |
| - | end |
| + | def entry_to_query_string(entry) |
| + | type, slug = entry.content_type_slug, entry._slug |
| + | "#{SUBMITTED_TYPE_PARAM}=#{type}&#{SUBMITTED_PARAM}=#{slug}" |
| end | |
| - | # Mimic the creation of a content entry with a minimal validation. |
| - | # |
| - | # @param [ String ] permalink The permalink (or slug) of the content type |
| - | # |
| - | # |
| - | def process_form(permalink) |
| - | permalink = permalink.split('.').first |
| + | def with_locale(&block) |
| + | locale = default_locale || params[:locale] |
| - | @content_type = self.mounting_point.content_types[permalink] |
| + | if path =~ /^\/(#{site.locales.join('|')})+(\/|$)/ |
| + | locale = $1 |
| + | end |
| - | raise "Unknown content type '#{@content_type.inspect}'" if @content_type.nil? |
| + | services.current_locale = locale |
| - | attributes = self.params[:entry] || self.params[:content] || {} |
| + | I18n.with_locale(locale, &block) |
| + | end |
| + | |
| + | def success_location(query); location(:success, query); end |
| + | def error_location; location(:error); end |
| - | @entry = @content_type.build_entry(attributes) |
| + | def location(state, query = '') |
| + | location = params[:"#{state}_callback"] || (entry_submissions_path? ? '/' : request.path_info) |
| - | # if not valid, we do not need to keep track of the entry |
| - | @content_type.entries.delete(@entry) if !@entry.valid? |
| + | if query.blank? |
| + | location |
| + | else |
| + | location += (location.include?('?') ? '&' : '?') + query |
| + | end |
| end | |
| - | def callback_url |
| - | (@entry.valid? ? params[:success_callback] : params[:error_callback]) || '/' |
| + | def entry_submissions_path? |
| + | !(request.path_info =~ ENTRY_SUBMISSION_REGEXP).nil? |
| end | |
| - | # Build the JSON response |
| + | # 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). |
| # | |
| - | # @param [ Integer ] status The HTTP return code |
| + | def get_content_type_slug |
| + | if request.post? && (request.path_info =~ ENTRY_SUBMISSION_REGEXP || params[:content_type_slug]) |
| + | $1 || params[:content_type_slug] |
| + | end |
| + | end |
| + | |
| + | # Create a content entry with a minimal validation. |
| + | # |
| + | # @param [ String ] slug The slug (or permalink) of the content type |
| # | |
| - | # @return [ Array ] The rack response depending on the validation status and the requested format |
| # | |
| - | def json_response(status = 200) |
| - | locale = self.mounting_point.default_locale |
| + | def create_entry(slug) |
| + | attributes = self.params[:entry] || self.params[:content] || {} |
| - | if self.request.path =~ /^\/(#{self.mounting_point.locales.join('|')})+(\/|$)/ |
| - | locale = $1 |
| + | if entry = services.entry_submission.submit(slug, attributes) |
| + | entry |
| + | else |
| + | raise "Unknown content type '#{slug}'" |
| end | |
| + | end |
| - | hash = @entry.to_hash(false).tap do |_hash| |
| - | if !@entry.valid? |
| - | _hash['errors'] = @entry.errors.inject({}) do |memo, name| |
| - | memo[name] = ::I18n.t('errors.messages.blank', locale: locale) |
| - | memo |
| - | end |
| + | # Get the content entry from the params. |
| + | # |
| + | def fetch_entry |
| + | if type_slug = params[SUBMITTED_TYPE_PARAM] && slug = params[SUBMITTED_PARAM] |
| + | if entry = services.entry_submission.find(type_slug, slug) |
| + | store_in_liquid(entry) |
| end | |
| end | |
| + | end |
| - | [status, { 'Content-Type' => 'application/json' }, [ |
| - | { @content_type.slug.singularize => hash }.to_json |
| - | ]] |
| + | # Build the JSON response |
| + | # |
| + | # @param [ Integer ] status The HTTP return code |
| + | # |
| + | # @return [ Array ] The rack response depending on the validation status and the requested format |
| + | # |
| + | def json_response(entry, status = 200) |
| + | json = services.entry_submission.to_json(entry) |
| + | [status, { 'Content-Type' => 'application/json' }, [json]] |
| end | |
| end | |
| end | |
| - | end |
| \ No newline at end of file | |
| + | end |
locomotive/steam/middlewares/helpers.rb b/lib/locomotive/steam/middlewares/helpers.rb
+2
-2
| @@ | @@ -18,8 +18,8 @@ module Locomotive::Steam |
| end | |
| def redirect_to(location, type = 301) | |
| - | self.log "Redirected to #{location}" |
| - | [type, { 'Content-Type' => 'text/html', 'Location' => location }, []] |
| + | self.log "Redirected to #{location}".blue |
| + | @next_response = [type, { 'Content-Type' => 'text/html', 'Location' => location }, []] |
| end | |
| def log(msg) | |
locomotive/steam/middlewares/locale.rb b/lib/locomotive/steam/middlewares/locale.rb
+14
-12
| @@ | @@ -12,27 +12,29 @@ module Locomotive::Steam |
| include Helpers | |
| def _call | |
| - | set_locale |
| + | locale = extract_locale |
| + | |
| + | log "Detecting locale #{locale.upcase}" |
| + | |
| + | I18n.with_locale(locale) do |
| + | self.next |
| + | end |
| end | |
| protected | |
| - | def set_locale |
| - | _locale = default_locale |
| - | _path = path |
| + | def extract_locale |
| + | _locale = params[:locale] || default_locale |
| + | _path = request.path_info |
| - | if _path =~ /^(#{site.locales.join('|')})+(\/|$)/ |
| + | if _path =~ /^\/(#{site.locales.join('|')})+(\/|$)/ |
| _locale = $1 | |
| _path = _path.gsub($1 + $2, '') | |
| - | _path = 'index' if _path.blank? |
| + | # _path = 'index' if _path.blank? # TODO |
| end | |
| - | log "Detecting locale #{_locale.upcase}" |
| - | |
| - | services.current_locale = _locale |
| - | |
| - | env['steam.locale'] = _locale |
| - | env['steam.path'] = _path |
| + | env['steam.path'] = _path |
| + | env['steam.locale'] = services.current_locale = _locale |
| end | |
| end | |
locomotive/steam/middlewares/path.rb b/lib/locomotive/steam/middlewares/path.rb
+1
-1
| @@ | @@ -14,7 +14,7 @@ module Locomotive::Steam |
| protected | |
| def set_path!(env) | |
| - | path = env['PATH_INFO'].clone |
| + | path = env['steam.path'] || request.path_info |
| path.gsub!(/\.[a-zA-Z][a-zA-Z0-9]{2,}$/, '') | |
| path.gsub!(/^\//, '') | |
locomotive/steam/middlewares/stack.rb b/lib/locomotive/steam/middlewares/stack.rb
+0
-99
| @@ | @@ -1,99 +0,0 @@ |
| - | # require 'rack/session/moneta' |
| - | # require 'rack/builder' |
| - | # require 'rack/lint' |
| - | # require 'dragonfly/middleware' |
| - | |
| - | # module Locomotive |
| - | # module Steam |
| - | # module Middlewares |
| - | |
| - | # class Stack |
| - | |
| - | # def initialize(options) |
| - | # @options = prepare_options(options) |
| - | # end |
| - | |
| - | # def create |
| - | # options = @options |
| - | # # _self = self |
| - | |
| - | # Rack::Builder.new do |
| - | # use Rack::Lint |
| - | |
| - | # use Steam::Middlewares::Favicon |
| - | |
| - | # # if options[:serve_assets] |
| - | # # use Steam::Middlewares::StaticAssets, { |
| - | # # urls: ['/images', '/fonts', '/samples', '/media'] |
| - | # # } |
| - | # # use Steam::Middlewares::DynamicAssets |
| - | # # end |
| - | |
| - | # # use Rack::Csrf, |
| - | # # field: 'authenticity_token', |
| - | # # skip_if: -> (request) { |
| - | # # !(request.post? && request.params[:content_type_slug].present?) |
| - | # # } |
| - | |
| - | # # use ::Dragonfly::Middleware, :steam |
| - | |
| - | # # use Rack::Session::Moneta, options[:moneta] |
| - | |
| - | # # _self.send(:use_steam_middlewares, builder) |
| - | |
| - | # use Middlewares::Logging |
| - | # use Middlewares::Path |
| - | |
| - | # # foo = proc do |env| |
| - | # # puts "[EndPoint] finishing here..." |
| - | # # [ 200, {'Content-Type' => 'text/plain'}, ["b"] ] |
| - | # # end |
| - | |
| - | # # run foo |
| - | |
| - | # run Steam::Middlewares::Renderer.new |
| - | # end |
| - | # end |
| - | |
| - | # protected |
| - | |
| - | # def use_steam_middlewares(builder) |
| - | # # builder.use Middlewares::Logging |
| - | # # builder.use Middlewares::Site |
| - | # # builder.use Middlewares::Path |
| - | |
| - | # # builder.run Steam::Middlewares::Renderer.new |
| - | |
| - | # # builder.instance_eval do |
| - | # # use Middlewares::Logging |
| - | |
| - | # # use Middlewares::Site |
| - | |
| - | # # # use Middlewares::EntrySubmission |
| - | |
| - | # # use Middlewares::Path |
| - | |
| - | # # nil |
| - | # # # use Middlewares::Locale |
| - | # # # use Middlewares::Timezone |
| - | |
| - | # # # use Middlewares::Page |
| - | # # # use Middlewares::TemplatizedPage |
| - | # # end |
| - | # # nil |
| - | # end |
| - | |
| - | # # def prepare_options(options) |
| - | # # { |
| - | # # serve_assets: false, |
| - | # # moneta: { |
| - | # # store: Moneta.new(:Memory, expires: true) |
| - | # # } |
| - | # # }.merge(options) |
| - | # # end |
| - | |
| - | # end |
| - | |
| - | # end |
| - | # end |
| - | # end |
locomotive/steam/middlewares/templatized_page.rb b/lib/locomotive/steam/middlewares/templatized_page.rb
+2
-2
| @@ | @@ -7,7 +7,7 @@ module Locomotive::Steam |
| def _call | |
| if page && page.templatized? | |
| - | self.set_content_entry! |
| + | set_content_entry! |
| end | |
| end | |
| @@ | @@ -20,7 +20,7 @@ module Locomotive::Steam |
| if entry = fetch_content_entry($1) | |
| # the entry will be available in the template under different keys | |
| ['content_entry', 'entry', entry.content_type.slug.singularize].each do |key| | |
| - | env['steam.liquid_assigns'][key] = entry |
| + | liquid_assigns[key] = entry |
| end | |
| env['steam.content_entry'] = page.content_entry = entry | |
locomotive/steam/middlewares/threadsafe.rb b/lib/locomotive/steam/middlewares/threadsafe.rb
+5
-1
| @@ | @@ -48,12 +48,16 @@ module Locomotive::Steam::Middlewares |
| @locale ||= env.fetch('steam.locale') | |
| end | |
| + | def liquid_assigns |
| + | @liquid_assigns ||= env.fetch('steam.liquid_assigns') |
| + | end |
| + | |
| def default_locale | |
| site.default_locale | |
| end | |
| def params | |
| - | @params ||= self.request.params #.deep_symbolize_keys |
| + | @params ||= HashConverter.to_sym(self.request.params) |
| end | |
| end | |
locomotive/steam/middlewares/timezone.rb b/lib/locomotive/steam/middlewares/timezone.rb
+0
-1
| @@ | @@ -12,7 +12,6 @@ module Locomotive::Steam |
| log "Timezone: #{timezone.inspect}" | |
| - | # DEBUG |
| Time.use_zone(timezone) do | |
| self.next | |
| end | |
locomotive/steam/repositories/filesystem.rb b/lib/locomotive/steam/repositories/filesystem.rb
+1
-0
| @@ | @@ -1,3 +1,4 @@ |
| + | require_relative 'filesystem/models/concerns/validation.rb' |
| require_relative 'filesystem/models/base' | |
| require_relative 'filesystem/concerns/queryable.rb' | |
| require_relative 'filesystem/yaml_loaders/concerns/common.rb' | |
locomotive/steam/repositories/filesystem/content_entry.rb b/lib/locomotive/steam/repositories/filesystem/content_entry.rb
+25
-0
| @@ | @@ -23,6 +23,31 @@ module Locomotive |
| end.all | |
| end | |
| + | # Engine: content_type.entries.build(attributes) |
| + | def build(type, attributes = {}) |
| + | collection_options[:model].new(attributes).tap do |entry| |
| + | # set the reference to the content type |
| + | entry.content_type = type |
| + | end |
| + | end |
| + | |
| + | # Engine: entry.save |
| + | def persist(entry) |
| + | return nil if entry.nil? |
| + | |
| + | collection = memoized_collection(entry.content_type) |
| + | |
| + | # slugify entry |
| + | sanitizer.set_slug(entry, collection) |
| + | |
| + | collection << entry |
| + | end |
| + | |
| + | # Engine: all(conditions).count > 0 |
| + | def exists?(type, conditions = {}) |
| + | query(type) { where(conditions) }.all.size > 0 |
| + | end |
| + | |
| # Engine: not necessary | |
| def by_slug(type, slug) | |
| query(type) { where(_slug: slug) }.first | |
locomotive/steam/repositories/filesystem/content_type.rb b/lib/locomotive/steam/repositories/filesystem/content_type.rb
+21
-3
| @@ | @@ -18,11 +18,29 @@ module Locomotive |
| end | |
| end | |
| + | # Engine: content_type.entries_custom_fields.where(unique: true) |
| + | def look_for_unique_fields(content_type) |
| + | return nil if content_type.nil? |
| + | |
| + | {}.tap do |hash| |
| + | content_type.query_fields { where(unique: true) }.each do |field| |
| + | hash[field.name] = field |
| + | end |
| + | end |
| + | end |
| + | |
| + | # Engine: content_type.entries_custom_fields |
| + | def fields_for(content_type) |
| + | return nil if content_type.nil? |
| + | |
| + | content_type.fields |
| + | end |
| + | |
| # Engine: content_type.entries.klass.send(:"#{name}_options").map { |option| option['name'] } | |
| - | def select_options(type, name) |
| - | return nil if type.nil? || name.nil? |
| + | def select_options(content_type, name) |
| + | return nil if content_type.nil? || name.nil? |
| - | field = type.fields_by_name[name] |
| + | field = content_type.fields_by_name[name] |
| if field.type == :select | |
| localized_attribute(field, :select_options) | |
locomotive/steam/repositories/filesystem/models/base.rb b/lib/locomotive/steam/repositories/filesystem/models/base.rb
+2
-0
| @@ | @@ -6,6 +6,8 @@ module Locomotive |
| class Base | |
| + | include Concerns::Validation |
| + | |
| attr_accessor :attributes | |
| def initialize(attributes) | |
locomotive/steam/repositories/filesystem/models/concerns/validation.rb b/lib/locomotive/steam/repositories/filesystem/models/concerns/validation.rb
+62
-0
| @@ | @@ -0,0 +1,62 @@ |
| + | require 'forwardable' |
| + | |
| + | module Locomotive |
| + | module Steam |
| + | module Repositories |
| + | module Filesystem |
| + | module Models |
| + | module Concerns |
| + | |
| + | module Validation |
| + | |
| + | def errors |
| + | @errors ||= Errors.new(self) |
| + | end |
| + | |
| + | def valid? |
| + | true |
| + | end |
| + | |
| + | class Errors |
| + | |
| + | include Enumerable |
| + | extend Forwardable |
| + | |
| + | attr_accessor :messages |
| + | |
| + | def_delegators :@messages, :[], :clear, :empty?, :each, :size |
| + | |
| + | alias_method :blank?, :empty? |
| + | |
| + | def initialize(base) |
| + | @base = base |
| + | @messages = HashWithIndifferentAccess.new({}) |
| + | end |
| + | |
| + | def add_on_blank(attribute) |
| + | value = @base.send(attribute) |
| + | add(attribute, :blank) if value.blank? |
| + | end |
| + | |
| + | def add(attribute, message) |
| + | (@messages[attribute] ||= []) << generate_message(message) |
| + | end |
| + | |
| + | def generate_message(message) |
| + | case message |
| + | when :blank, :unique then I18n.t(message, scope: 'errors.messages') |
| + | else |
| + | message |
| + | end |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
locomotive/steam/repositories/filesystem/models/content_entry.rb b/lib/locomotive/steam/repositories/filesystem/models/content_entry.rb
+9
-0
| @@ | @@ -37,6 +37,15 @@ module Locomotive |
| end | |
| end | |
| + | def valid? |
| + | errors.clear |
| + | content_type.fields_by_name.each do |name, field| |
| + | next unless field.required? |
| + | errors.add_on_blank(name.to_sym) |
| + | end |
| + | errors.empty? |
| + | end |
| + | |
| def content_type | |
| @content_type || attributes[:content_type] | |
| end | |
locomotive/steam/repositories/filesystem/models/content_type_field.rb b/lib/locomotive/steam/repositories/filesystem/models/content_type_field.rb
+6
-1
| @@ | @@ -9,7 +9,9 @@ module Locomotive |
| def initialize(attributes) | |
| super({ | |
| type: :string, | |
| - | localized: false |
| + | localized: false, |
| + | required: false, |
| + | unique: false |
| }.merge(attributes)) | |
| end | |
| @@ | @@ -17,6 +19,9 @@ module Locomotive |
| self[:class_name] || self[:target] | |
| end | |
| + | def required?; self[:required]; end |
| + | def localized?; self[:localized]; end |
| + | |
| end | |
| end | |
locomotive/steam/repositories/filesystem/sanitizers/content_type.rb b/lib/locomotive/steam/repositories/filesystem/sanitizers/content_type.rb
+1
-1
| @@ | @@ -20,7 +20,7 @@ module Locomotive |
| list.map do |attributes| | |
| name, _attributes = attributes.keys.first, attributes.values.first | |
| - | _attributes[:name] = name |
| + | _attributes[:name] = name.to_sym |
| if _attributes[:label].blank? | |
| _attributes[:label] = name.to_s.humanize | |
locomotive/steam/server.rb b/lib/locomotive/steam/server.rb
+3
-2
| @@ | @@ -34,9 +34,10 @@ module Locomotive::Steam |
| use Middlewares::DefaultEnv, server.options | |
| use Middlewares::Logging | |
| use Middlewares::Site | |
| - | use Middlewares::Path |
| - | use Middlewares::Locale |
| use Middlewares::Timezone | |
| + | use Middlewares::EntrySubmission |
| + | use Middlewares::Locale |
| + | use Middlewares::Path |
| use Middlewares::Page | |
| use Middlewares::TemplatizedPage | |
locomotive/steam/services.rb b/lib/locomotive/steam/services.rb
+4
-0
| @@ | @@ -38,6 +38,10 @@ module Locomotive |
| Steam::Services::SnippetFinder.new(repositories.snippet) | |
| end | |
| + | register :entry_submission do |
| + | Steam::Services::EntrySubmission.new(repositories.content_type, repositories.content_entry, current_locale) |
| + | end |
| + | |
| register :liquid_parser do | |
| Steam::Services::LiquidParser.new(parent_finder, snippet_finder) | |
| end | |
locomotive/steam/services/concerns/decorator.rb b/lib/locomotive/steam/services/concerns/decorator.rb
+6
-2
| @@ | @@ -7,14 +7,18 @@ module Locomotive |
| private | |
| - | def decorate(&block) |
| + | def decorate(klass = Decorators::TemplateDecorator, &block) |
| if (object = yield).blank? | |
| object | |
| else | |
| - | Decorators::TemplateDecorator.decorate(object, nil, locale, default_locale) |
| + | klass.decorate(object, nil, locale, default_locale) |
| end | |
| end | |
| + | def i18n_decorate(&block) |
| + | decorate(Decorators::I18nDecorator, &block) |
| + | end |
| + | |
| def locale | |
| repository.current_locale | |
| end | |
locomotive/steam/services/entry_submission.rb b/lib/locomotive/steam/services/entry_submission.rb
+81
-0
| @@ | @@ -0,0 +1,81 @@ |
| + | module Locomotive |
| + | module Steam |
| + | module Services |
| + | |
| + | class EntrySubmission < Struct.new(:content_type_repository, :repository, :current_locale) |
| + | |
| + | include Locomotive::Steam::Services::Concerns::Decorator |
| + | |
| + | def submit(slug, attributes = {}) |
| + | type = get_type(slug) |
| + | |
| + | return nil if type.nil? |
| + | |
| + | build_entry(type, attributes) do |entry| |
| + | if validate(entry) |
| + | repository.persist(entry) |
| + | end |
| + | end |
| + | end |
| + | |
| + | def find(type_slug, slug) |
| + | type = get_type(slug) |
| + | |
| + | return nil if type.nil? |
| + | |
| + | i18n_decorate { repository.by_slug(type, slug) } |
| + | end |
| + | |
| + | def to_json(entry) |
| + | return nil if entry.nil? |
| + | |
| + | # default values |
| + | hash = { _slug: entry._slug, content_type_slug: entry.content_type_slug } |
| + | |
| + | # dynamic attributes |
| + | content_type_repository.fields_for(entry.content_type).each do |field| |
| + | next if %w(belongs_to has_many many_to_many).include?(field.type.to_s) |
| + | |
| + | hash[field.name] = entry.send(field.name) |
| + | end |
| + | |
| + | # errors |
| + | hash[:errors] = entry.errors.messages unless entry.errors.empty? |
| + | |
| + | hash.to_json |
| + | end |
| + | |
| + | private |
| + | |
| + | def get_type(slug) |
| + | return nil if slug.blank? |
| + | |
| + | content_type_repository.by_slug(slug) |
| + | end |
| + | |
| + | def build_entry(type, attributes, &block) |
| + | i18n_decorate { repository.build(type, attributes) }.tap do |entry| |
| + | yield(entry) |
| + | end |
| + | end |
| + | |
| + | def validate(entry) |
| + | # simple validations (existence of values) first |
| + | entry.valid? |
| + | |
| + | # check if the entry has unique values for its |
| + | # fields marked as unique are really |
| + | content_type_repository.look_for_unique_fields(entry.content_type).each do |name, field| |
| + | if repository.exists?(entry.content_type, name, entry.send(name)) |
| + | entry.errors.add(name, :unique) |
| + | end |
| + | end |
| + | |
| + | entry.errors.empty? |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
spec/integration/server/contact_form_spec.rb
+70
-70
| @@ | @@ -1,107 +1,107 @@ |
| - | # require File.dirname(__FILE__) + '/../integration_helper' |
| + | require File.dirname(__FILE__) + '/../integration_helper' |
| - | # describe 'ContactForm', pending: true do |
| + | describe 'ContactForm' do |
| - | # include Rack::Test::Methods |
| + | include Rack::Test::Methods |
| - | # def app |
| - | # run_server |
| - | # end |
| + | def app |
| + | run_server |
| + | end |
| - | # it 'renders the form' do |
| - | # get '/contact' |
| - | # last_response.body.should =~ /\/entry_submissions\/messages.json/ |
| - | # end |
| + | it 'renders the form' do |
| + | get '/contact' |
| + | expect(last_response.body).to include '/entry_submissions/messages.json' |
| + | end |
| - | # describe '#submit' do |
| + | # describe '#submit' do |
| - | # let(:params) { { |
| - | # 'entry' => { 'name' => 'John', 'email' => 'j@doe.net', 'message' => 'Bla bla' }, |
| - | # 'success_callback' => '/events', |
| - | # 'error_callback' => '/contact' } } |
| - | # let(:response) { post_contact_form(params, false) } |
| - | # let(:status) { response.status } |
| + | # let(:params) { { |
| + | # 'entry' => { 'name' => 'John', 'email' => 'j@doe.net', 'message' => 'Bla bla' }, |
| + | # 'success_callback' => '/events', |
| + | # 'error_callback' => '/contact' } } |
| + | # let(:response) { post_contact_form(params, false) } |
| + | # let(:status) { response.status } |
| - | # describe 'with json request' do |
| + | # describe 'with json request' do |
| - | # let(:response) { post_contact_form(params, true) } |
| - | # let(:entry) { JSON.parse(response.body)['message'] } |
| + | # let(:response) { post_contact_form(params, true) } |
| + | # let(:entry) { JSON.parse(response.body) } |
| - | # context 'when not valid' do |
| + | # context 'when not valid' do |
| - | # let(:params) { {} } |
| + | # let(:params) { {} } |
| - | # it 'returns an error status' do |
| - | # response.status.should == 422 |
| - | # end |
| + | # it 'returns an error status' do |
| + | # expect(response.status).to eq 422 |
| + | # end |
| - | # describe 'errors' do |
| + | # describe 'errors' do |
| - | # subject { entry['errors'] } |
| + | # subject { entry['errors'] } |
| - | # it { should have_key_with_value('name', "can't not be blank") } |
| + | # it { should have_key_with_value('name', "can't not be blank") } |
| - | # it { should have_key_with_value('email', "can't not be blank") } |
| + | # it { should have_key_with_value('email', "can't not be blank") } |
| - | # it { should have_key_with_value('message', "can't not be blank") } |
| + | # it { should have_key_with_value('message', "can't not be blank") } |
| - | # end |
| + | # end |
| - | # end |
| + | # end |
| - | # context 'when valid' do |
| + | # context 'when valid' do |
| - | # it 'returns a success status' do |
| - | # response.status.should == 200 |
| - | # end |
| + | # it 'returns a success status' do |
| + | # response.status.should == 200 |
| + | # end |
| - | # end |
| + | # end |
| - | # end |
| + | # end |
| - | # describe 'with html request' do |
| + | # describe 'with html request' do |
| - | # context 'when not valid' do |
| + | # context 'when not valid' do |
| - | # let(:params) { { 'error_callback' => '/contact' } } |
| + | # let(:params) { { 'error_callback' => '/contact' } } |
| - | # it 'returns a success status' do |
| - | # response.status.should == 200 |
| - | # end |
| + | # 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 |
| + | # it 'displays errors' do |
| + | # response.body.to_s.should =~ /can't not be blank/ |
| + | # end |
| - | # end |
| + | # end |
| - | # context 'when valid' do |
| + | # context 'when valid' do |
| - | # let(:response) { post_contact_form(params, false, true) } |
| + | # let(:response) { post_contact_form(params, false, true) } |
| - | # it 'returns a success status' do |
| - | # response.status.should == 200 |
| - | # end |
| + | # it 'returns a success status' do |
| + | # response.status.should == 200 |
| + | # end |
| - | # it 'displays a success message' do |
| - | # response.body.should =~ /Thank you John/ |
| - | # end |
| + | # it 'displays a success message' do |
| + | # response.body.should =~ /Thank you John/ |
| + | # end |
| - | # end |
| + | # end |
| - | # end |
| + | # end |
| - | # end |
| + | # end |
| - | # def post_contact_form(params, json = false, follow_redirect = false) |
| - | # url = '/entry_submissions/messages' |
| - | # url += '.json' if json |
| - | # params = params.symbolize_keys if json |
| - | # post url, params |
| - | # if follow_redirect |
| - | # follow_redirect! |
| - | # end |
| - | # last_response |
| - | # end |
| + | # def post_contact_form(params, json = false, follow_redirect = false) |
| + | # url = '/entry_submissions/messages' |
| + | # url += '.json' if json |
| + | # params = params.symbolize_keys if json |
| + | # post url, params |
| + | # if follow_redirect |
| + | # follow_redirect! |
| + | # end |
| + | # last_response |
| + | # end |
| - | # end |
| + | end |
spec/unit/repositories/filesystem/content_entry_spec.rb
+46
-0
| @@ | @@ -39,6 +39,52 @@ describe Locomotive::Steam::Repositories::Filesystem::ContentEntry do |
| end | |
| + | describe '#build' do |
| + | |
| + | let(:attributes) { { title: 'Hello world' } } |
| + | subject { repository.build(type, attributes) } |
| + | |
| + | it { expect(subject.title).to eq 'Hello world' } |
| + | |
| + | end |
| + | |
| + | describe '#persist' do |
| + | |
| + | let(:entry) { instance_double('NewEntry', _visible: true, content_type: type, _label: 'Hello world') } |
| + | subject { repository.persist(entry) } |
| + | |
| + | before do |
| + | expect(entry).to receive(:[]).with(:_slug).and_return(nil) |
| + | expect(entry).to receive(:[]=).with(:_slug, 'hello-world') |
| + | end |
| + | |
| + | it { expect { subject }.to change { repository.all(type).size }.by(1) } |
| + | |
| + | end |
| + | |
| + | describe '#exists?' do |
| + | |
| + | let(:conditions) { {} } |
| + | subject { repository.exists?(type, conditions) } |
| + | |
| + | it { expect(subject).to eq true } |
| + | |
| + | context 'more specific conditions' do |
| + | |
| + | let(:conditions) { { '_slug' => 'update-number-1' } } |
| + | it { expect(subject).to eq true } |
| + | |
| + | end |
| + | |
| + | context 'conditions which do match any entries' do |
| + | |
| + | let(:conditions) { { '_slug' => 'foo' } } |
| + | it { expect(subject).to eq false } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| describe '#by_slug' do | |
| let(:slug) { nil } | |
spec/unit/repositories/filesystem/content_type_spec.rb
+34
-0
| @@ | @@ -62,6 +62,40 @@ describe Locomotive::Steam::Repositories::Filesystem::ContentType do |
| end | |
| + | describe '#fields_for' do |
| + | |
| + | let(:type) { nil } |
| + | subject { repository.fields_for(type) } |
| + | |
| + | it { is_expected.to eq nil } |
| + | |
| + | context 'with fields' do |
| + | |
| + | let(:type) { instance_double('ContentType', fields: [true]) } |
| + | it { is_expected.to eq([true]) } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | describe '#look_for_unique_fields' do |
| + | |
| + | let(:type) { nil } |
| + | subject { repository.look_for_unique_fields(type) } |
| + | |
| + | it { is_expected.to eq nil } |
| + | |
| + | context 'with fields' do |
| + | |
| + | let(:field) { instance_double('Field', name: :title) } |
| + | let(:type) { instance_double('ContentType', query_fields: [field])} |
| + | |
| + | it { expect(subject).to eq(title: field) } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| describe '#select_options' do | |
| let(:type) { repository.by_slug('articles') } | |
spec/unit/repositories/filesystem/models/content_entry_spec.rb
+20
-1
| @@ | @@ -2,7 +2,8 @@ require 'spec_helper' |
| describe Locomotive::Steam::Repositories::Filesystem::Models::ContentEntry do | |
| - | let(:type) { instance_double('ContentType', slug: 'articles', label_field_name: :title, localized_fields_names: [:title], fields_by_name: {}) } |
| + | let(:fields) { {} } |
| + | let(:type) { instance_double('ContentType', slug: 'articles', label_field_name: :title, localized_fields_names: [:title], fields_by_name: fields) } |
| let(:attributes) { { title: 'Hello world', _slug: 'hello-world' } } | |
| let(:content_entry) do | |
| Locomotive::Steam::Repositories::Filesystem::Models::ContentEntry.new(attributes).tap do |entry| | |
| @@ | @@ -10,6 +11,24 @@ describe Locomotive::Steam::Repositories::Filesystem::Models::ContentEntry do |
| end | |
| end | |
| + | describe '#valid?' do |
| + | |
| + | let(:fields) { { title: instance_double('TitleField', name: :title, type: :string, required?: true)} } |
| + | |
| + | subject { content_entry.valid? } |
| + | it { is_expected.to eq true } |
| + | |
| + | context 'missing attribute' do |
| + | |
| + | let(:attributes) { {} } |
| + | it { is_expected.to eq false } |
| + | it { subject; expect(content_entry.errors[:title]).to eq(["can't not be blank"]) } |
| + | it { subject; expect(content_entry.errors.empty?).to eq false } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| describe '#_label' do | |
| subject { content_entry._label } | |
spec/unit/services/entry_submission_spec.rb
+141
-0
| @@ | @@ -0,0 +1,141 @@ |
| + | require 'spec_helper' |
| + | |
| + | describe Locomotive::Steam::Services::EntrySubmission do |
| + | |
| + | let(:site) { instance_double('Site', default_locale: 'en') } |
| + | let(:locale) { 'en' } |
| + | let(:type_repository) { Locomotive::Steam::Repositories::Filesystem::ContentType.new(nil, site) } |
| + | let(:entry_repository) { Locomotive::Steam::Repositories::Filesystem::ContentEntry.new(nil, site) } |
| + | let(:service) { Locomotive::Steam::Services::EntrySubmission.new(type_repository, entry_repository, locale) } |
| + | |
| + | describe '#find' do |
| + | |
| + | let(:type_slug) { 'articles' } |
| + | let(:slug) { 'hello-world' } |
| + | subject { service.find(type_slug, slug) } |
| + | |
| + | context 'unknown content type' do |
| + | |
| + | before { allow(type_repository).to receive(:by_slug).and_return(nil) } |
| + | it { is_expected.to eq nil } |
| + | |
| + | end |
| + | |
| + | context 'existing content type' do |
| + | |
| + | let(:type) { instance_double('Articles') } |
| + | let(:entry) { instance_double('Entry', title: 'Hello world', content_type: type, attributes: { title: 'Hello world' }, localized_attributes: []) } |
| + | |
| + | before do |
| + | allow(type_repository).to receive(:by_slug).and_return(type) |
| + | allow(entry_repository).to receive(:by_slug).with(type, 'hello-world').and_return(entry) |
| + | end |
| + | |
| + | it { is_expected.to eq entry } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | describe '#to_json' do |
| + | |
| + | let(:entry) { nil } |
| + | subject { service.to_json(entry) } |
| + | |
| + | it { is_expected.to eq nil } |
| + | |
| + | context 'existing content entry' do |
| + | |
| + | let(:errors) { {} } |
| + | let(:fields) { [instance_double('TitleField', name: :title, type: :string)] } |
| + | let(:type) { instance_double('Articles') } |
| + | let(:entry) { instance_double('DecoratedEntry', _slug: 'hello-world', title: 'Hello world', content_type: type, content_type_slug: :articles, errors: errors) } |
| + | |
| + | before { allow(type_repository).to receive(:fields_for).with(type).and_return(fields) } |
| + | |
| + | it { is_expected.to eq '{"_slug":"hello-world","content_type_slug":"articles","title":"Hello world"}' } |
| + | |
| + | context 'with errors' do |
| + | |
| + | let(:errors) { instance_double('Errors', empty?: false, messages: { title: ["can't be blank"] }) } |
| + | it { is_expected.to eq '{"_slug":"hello-world","content_type_slug":"articles","title":"Hello world","errors":{"title":["can\'t be blank"]}}' } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | describe '#submit' do |
| + | |
| + | let(:slug) { nil } |
| + | let(:attributes) { { title: 'Hello world' } } |
| + | subject { service.submit(slug, attributes) } |
| + | |
| + | it { is_expected.to eq nil } |
| + | |
| + | context 'unknown content type' do |
| + | |
| + | let(:slug) { 'articles' } |
| + | |
| + | before { allow(type_repository).to receive(:by_slug).with('articles').and_return nil } |
| + | |
| + | it { is_expected.to eq nil } |
| + | |
| + | end |
| + | |
| + | context 'existing content type' do |
| + | |
| + | let(:unique_fields) { {} } |
| + | let(:first_validation) { false } |
| + | let(:errors) { [:title] } |
| + | let(:type) { instance_double('Comments') } |
| + | let(:entry) { instance_double('Entry', title: 'Hello world', content_type: type, valid?: first_validation, errors: errors, attributes: { title: 'Hello world' }, localized_attributes: []) } |
| + | let(:slug) { 'comments' } |
| + | |
| + | before do |
| + | allow(type_repository).to receive(:by_slug).and_return(type) |
| + | allow(type_repository).to receive(:look_for_unique_fields).and_return(unique_fields) |
| + | allow(entry_repository).to receive(:build).with(type, attributes).and_return(entry) |
| + | end |
| + | |
| + | context 'valid' do |
| + | |
| + | before { expect(entry_repository).to receive(:persist) } |
| + | |
| + | let(:first_validation) { true } |
| + | let(:errors) { {} } |
| + | |
| + | it { is_expected.to eq entry } |
| + | it { expect(subject.errors.empty?).to eq true } |
| + | |
| + | end |
| + | |
| + | context 'not valid' do |
| + | |
| + | before { expect(entry_repository).not_to receive(:persist) } |
| + | |
| + | it { is_expected.to eq entry } |
| + | it { expect(subject.errors).to eq([:title]) } |
| + | |
| + | context 'with unique fields' do |
| + | |
| + | let(:unique_fields) { { title: instance_double('Field', name: 'title') } } |
| + | |
| + | before do |
| + | allow(entry_repository).to receive(:exists?).with(type, :title, 'Hello world').and_return(true) |
| + | expect(entry.errors).to receive(:add).with(:title, :unique).and_return(true) |
| + | end |
| + | |
| + | it { is_expected.to eq entry } |
| + | it { expect(subject.errors).to eq([:title]) } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | end |