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