Skip to content

Instantly share code, notes, and snippets.

@cfitz
Created April 10, 2015 08:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cfitz/92d1d17aa006a0043dca to your computer and use it in GitHub Desktop.
Save cfitz/92d1d17aa006a0043dca to your computer and use it in GitHub Desktop.
aspace_form_helper.rb
#
# Add this file to plugins/local/frontend/controllers/aspace_forms_helper.rb
module AspaceFormHelper
COMBOBOX_MIN_LIMIT = 50 # if a <select> has equal or more options than this value, output a combobox
class FormContext
def initialize(name, values_from, parent)
values = values_from.is_a?(JSONModelType) ? values_from.to_hash(:raw) : values_from
@forms = Object.new
@parent = parent
@context = [[name, values]]
@path_to_i18n_map = {}
class << @forms
include ActionView::Helpers::TagHelper
include ActionView::Helpers::TextHelper
include ActionView::Helpers::FormTagHelper
include ActionView::Helpers::FormOptionsHelper
end
end
def h(str)
ERB::Util.html_escape(str)
end
def readonly?
false
end
def path_to_i18n_map
@path_to_i18n_map
end
def set_index(template, idx)
template.gsub(/\[\]$/, "[#{idx}]")
end
def list_for(objects, context_name, &block)
objects ||= []
result = ""
objects.each_with_index do |object, idx|
push(set_index(context_name, idx), object) do
result << "<li id=\"#{current_id}\" class=\"subrecord-form-wrapper\" data-index=\"#{idx}\" data-object-name=\"#{context_name.gsub(/\[\]/,"").singularize}\">"
result << hidden_input("lock_version") if obj.respond_to?(:has_key?) && obj.has_key?("lock_version")
result << @parent.capture(object, idx, &block)
result << "</li>"
end
end
("<ul data-name-path=\"#{set_index(self.path(context_name), '${index}')}\" " +
" data-id-path=\"#{id_for(set_index(self.path(context_name), '${index}'), false)}\" " +
" class=\"subrecord-form-list\">#{result}</ul>").html_safe
end
def fields_for(object, context_name, &block)
result = ""
push(context_name, object) do
result << hidden_input("lock_version", object["lock_version"]) if object
result << @parent.capture(object, &block)
end
("<div data-name-path=\"#{set_index(self.path(context_name), '${index}')}\" " +
" data-id-path=\"#{id_for(set_index(self.path(context_name), '${index}'), false)}\" " +
" class=\"subrecord-form-fields-for\">#{result}</div>").html_safe
end
def form_top
@context[0].first
end
def id
"form_#{form_top}"
end
# Turn a name like my[nested][object][0][title] into the equivalent JSON
# path (my/nested/object/0/title)
def name_to_json_path(name)
name.gsub(/[\[\]]+/, "/").gsub(/\/+$/, "").gsub(/^\/+/, "")
end
def path(name = nil)
names = @context.map(&:first)
tail = names.drop(1)
tail += [name] if name
path = tail.map {|e|
if e =~ /(.*?)\[([0-9]+)?\]$/
"[#{$1}][#{$2}]"
else
"[#{e}]"
end
}.join("")
if name
@path_to_i18n_map[name_to_json_path(path)] = i18n_for(name)
end
"#{names.first}#{path}"
end
def help_path_for(name)
names = @context.map(&:first)
return "#{names[-1].to_s.gsub(/\[.*\]/, "").singularize}_#{name}" if names.length > 0
name
end
def parent_context
form_top
end
def current_context
@context.last
end
def obj
@context.last.second
end
def [](key)
obj[key]
end
def push(name, values_from = {})
path(name) # populate the i18n mapping
@context.push([name, values_from])
yield(self)
@context.pop
end
def i18n_for(name)
"#{@active_template or form_top}.#{name.to_s.gsub(/\[\]$/, "")}"
end
def path_to_i18n_key(path)
path_to_i18n_map[path]
end
def exceptions_for_js(exceptions)
result = {}
[:errors, :warnings].each do |condition|
if exceptions[condition]
result[condition] = exceptions[condition].keys.map {|property|
id_for_javascript(property)
}
end
end
result.to_json.html_safe
end
def id_for_javascript(name)
"#{form_top}#{name.split("/").collect{|a| "[#{a}]"}.join}".gsub(/[\[\]\/]/, "_")
end
def current_id
path(nil).gsub(/[\[\]]/, '_')
end
def id_for(name, qualify = true)
name = path(name) if qualify
name.gsub(/[\[\]]/, '_')
end
def label_and_textfield(name, opts = {})
label_with_field(name, textfield(name, obj[name], opts[:field_opts] || {}), opts)
end
def label_and_date(name, opts = {})
field_opts = (opts[:field_opts] || {}).merge({
:class => "date-field form-control",
:"data-format" => "yyyy-mm-dd",
:"data-date" => Date.today.strftime('%Y-%m-%d'),
:"data-autoclose" => true,
:"data-force-parse" => false
})
if obj[name].blank? && opts[:default]
value = opts[:default]
else
value = obj[name]
end
opts[:col_size] = 4
date_input = textfield(name, value, field_opts)
label_with_field(name, date_input, opts)
end
def label_and_textarea(name, opts = {})
label_with_field(name, textarea(name, obj[name] || opts[:default], opts[:field_opts] || {}), opts)
end
def label_and_select(name, options, opts = {})
options = ([""] + options) if opts[:nodefault]
opts[:field_opts] ||= {}
opts[:col_size] = 4
widget = options.length < COMBOBOX_MIN_LIMIT ? select(name, options, opts[:field_opts] || {}) : combobox(name, options, opts[:field_opts] || {})
label_with_field(name, widget, opts)
end
def label_and_password(name, opts = {})
label_with_field(name, password(name, obj[name], opts[:field_opts] || {}), opts)
end
def label_and_boolean(name, opts = {}, default = false, force_checked = false)
opts[:col_size] = 1
opts[:controls_class] = "checkbox"
label_with_field(name, checkbox(name, opts, default, force_checked), opts)
end
def label_and_readonly(name, default = "", opts = {})
value = obj[name]
if opts.has_key? :controls_class
opts[:controls_class] << " label-only"
else
opts[:controls_class] = " label-only"
end
label_with_field(name, value.blank? ? default : value , opts)
end
def combobox(name, options, opts = {})
select(name, options, opts.merge({:"data-combobox" => true}))
end
def select(name, options, opts = {})
if opts.has_key? :class
opts[:class] << " form-control"
else
opts[:class] = "form-control"
end
@forms.select_tag(path(name), @forms.options_for_select(options, obj[name] || default_for(name) || opts[:default]), {:id => id_for(name)}.merge!(opts))
end
def textarea(name = nil, value = "", opts = {})
options = {:id => id_for(name), :rows => 3}
placeholder = I18n.t("#{i18n_for(name)}_placeholder", :default => '')
options[:placeholder] = placeholder if not placeholder.empty?
options[:class] = "form-control"
@forms.text_area_tag(path(name), h(value), options.merge(opts))
end
def textfield(name = nil, value = nil, opts = {})
value ||= obj[name] if !name.nil?
options = {:id => id_for(name), :type => "text", :value => h(value), :name => path(name)}
placeholder = I18n.t("#{i18n_for(name)}_placeholder", :default => '')
options[:placeholder] = placeholder if not placeholder.empty?
options[:class] = "form-control"
value = @forms.tag("input", options.merge(opts),
false, false)
if opts[:automatable]
by_default = default_for("#{name}_auto_generate") || false
value << "<label>".html_safe
value << checkbox("#{name}_auto_generate", {
:class => "automate-field-toggle", :display_text_when_checked => I18n.t("states.auto_generated")
}, by_default, false)
value << "&#160;<small>".html_safe
value << I18n.t("actions.automate")
value << "</small></label>".html_safe
end
inline_help = I18n.t("#{i18n_for(name)}_inline_help", :default => '')
if !inline_help.empty?
value << "<span class=\"help-inline\">#{inline_help}</span>".html_safe
end
value
end
def password(name = nil, value = "", opts = {})
@forms.tag("input", {:id => id_for(name), :type => "password", :value => h(value), :name => path(name)}.merge(opts),
false, false)
end
def hidden_input(name, value = nil, field_opts = {})
value = obj[name] if value.nil?
full_name = path(name)
if value && value.is_a?(Hash) && value.has_key?('ref')
full_name += '[ref]'
value = value['ref']
end
@forms.tag("input", {:id => id_for(name), :type => "hidden", :value => h(value), :name => full_name}.merge(field_opts),
false, false)
end
def emit_template(name, *args)
if !@parent.templates[name]
raise "No such template: #{name.inspect}"
end
old = @active_template
@active_template = name
@parent.templates[name][:block].call(self, *args)
@active_template = old
end
def label_and_fourpartid
field_html = textfield("id_0", obj["id_0"], :class => "id_0 form-control", :size => 10)
field_html << textfield("id_1", obj["id_1"], :class => "id_1 form-control", :size => 10, :disabled => obj["id_0"].blank? && obj["id_1"].blank?)
field_html << textfield("id_2", obj["id_2"], :class => "id_2 form-control", :size => 10, :disabled => obj["id_1"].blank? && obj["id_2"].blank?)
field_html << textfield("id_3", obj["id_3"], :class => "id_3 form-control", :size => 10, :disabled => obj["id_2"].blank? && obj["id_3"].blank?)
@forms.content_tag(:div, (I18n.t(i18n_for("id_0")) + field_html).html_safe, :class=> "identifier-fields")
label_with_field("id_0", field_html, :control_class => "identifier-fields")
end
def label(name, opts = {}, classes = [])
prefix = opts[:plugin] ? 'plugins.' : ''
classes << 'control-label'
options = {:class => classes.join(' '), :for => id_for(name)}
tooltip = I18n.t_raw("#{prefix}#{i18n_for(name)}_tooltip", :default => '')
if not tooltip.empty?
options[:title] = tooltip
options["data-placement"] = "bottom"
options["data-html"] = true
options["data-delay"] = 500
options["data-trigger"] = "manual"
options["data-template"] = '<div class="tooltip archivesspace-help"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
options[:class] += " has-tooltip"
end
@forms.content_tag(:label, I18n.t(prefix + i18n_for(name)), options.merge(opts || {}))
end
def checkbox(name, opts = {}, default = true, force_checked = false)
options = {:id => "#{id_for(name)}", :type => "checkbox", :name => path(name), :value => 1}
options[:checked] = "checked" if force_checked or (obj[name] === true) or (obj[name] === "true") or (obj[name].nil? and default)
@forms.tag("input", options.merge(opts), false, false)
end
def required?(name)
if @active_template && @parent.templates[@active_template]
@parent.templates[@active_template][:definition].required?(name)
else
false
end
end
def default_for(name)
if @active_template && @parent.templates[@active_template]
@parent.templates[@active_template][:definition].default_for(name)
else
nil
end
end
def allowable_types_for(name)
if @active_template && @parent.templates[@active_template]
@parent.templates[@active_template][:definition].allowable_types_for(name)
else
[]
end
end
def possible_options_for(name, add_empty_options = false, opts = {})
if @active_template && @parent.templates[@active_template]
@parent.templates[@active_template][:definition].options_for(self, name, add_empty_options, opts)
else
[]
end
end
def label_with_field(name, field_html, opts = {})
opts[:label_opts] ||= {}
opts[:label_opts][:plugin] = opts[:plugin]
opts[:col_size] ||= 9
control_group_classes,
label_classes,
controls_classes = %w(form-group), [], []
unless opts[:layout] && opts[:layout] == 'stacked'
label_classes << 'col-sm-2'
controls_classes << "col-sm-#{opts[:col_size]}"
end
# There must be a better way to say this...
# The value of the 'required' option wins out if set to either true or false
# if not specified, we take the value of required?
required = [:required, 'required'].map {|r| opts[r]}.compact.first
if required.nil?
required = required?(name)
end
control_group_classes << "required" if required == true
control_group_classes << "conditionally-required" if required == :conditionally
control_group_classes << "#{opts[:control_class]}" if opts.has_key? :control_class
controls_classes << "#{opts[:controls_class]}" if opts.has_key? :controls_class
control_group = "<div class=\"#{control_group_classes.join(' ')}\">"
control_group << label(name, opts[:label_opts], label_classes)
control_group << "<div class=\"#{controls_classes.join(' ')}\">"
control_group << field_html
control_group << "</div>"
control_group << "</div>"
control_group.html_safe
end
end
class ReadOnlyContext < FormContext
def readonly?
true
end
def select(name, options, opts = {})
return nil if obj[name].blank?
# Attempt a match in the options to give dynamic enums a chance.
match = options.find {|label, value| value == obj[name]}
if match
match[0]
else
I18n.t("#{i18n_for(name)}_#{obj[name]}", :default => obj[name])
end
end
def textfield(name = nil, value = "", opts = {})
return "" if value.blank?
opts[:escape] = true unless opts[:escape] == false
opts[:base_url] ||= "/"
value = MixedContentParser::parse(value, opts[:base_url]) if opts[:clean] == true
value = @parent.preserve_newlines(value) if opts[:clean] == true
value = CGI::escapeHTML(value) if opts[:escape]
value.html_safe
end
def textarea(name = nil, value = "", opts = {})
return "" if value.blank?
opts[:escape] = true unless opts[:escape] == false
opts[:base_url] ||= "/"
value = MixedContentParser::parse(value, opts[:base_url]) if opts[:clean] == true
value = @parent.preserve_newlines(value) if opts[:clean] == true
value = CGI::escapeHTML(value) if opts[:escape]
value.html_safe
end
def checkbox(name, opts = {}, default = true, force_checked = false)
((obj[name] === true) || obj[name] === "true") ? "True" : "False"
end
def label_with_field(name, field_html, opts = {})
return "" if field_html.blank?
super(name, field_html, opts.merge({:controls_class => "label-only"}))
end
def label_and_fourpartid
fourpart_html = "<div class='identifier-display'>"+
"<span class='identifier-display-part'>#{obj["id_0"]}</span>" +
"<span class='identifier-display-part'>#{obj["id_1"]}</span>" +
"<span class='identifier-display-part'>#{obj["id_2"]}</span>" +
"<span class='identifier-display-part'>#{obj["id_3"]}</span>" +
"</div>"
label_with_field("id_0", fourpart_html)
end
def label_and_date(name, opts = {})
label_with_field(name, "#{obj[name]}")
end
end
def form_context(name, values_from = {}, &body)
context = FormContext.new(name, values_from, self)
env = self.request.env
env['form_context_depth'] ||= 0
# Not feeling great about this, but we render the form twice: the first pass
# sets up the mapping from form input names to i18n keys, while the second
# actually uses that map to set the labels correctly.
env['form_context_depth'] += 1
capture(context, &body)
env['form_context_depth'] -= 1
s = "<div class=\"form-context\" id=\"form_#{name}\">".html_safe
s << context.hidden_input("lock_version", values_from["lock_version"])
env['form_context_depth'] += 1
s << capture(context, &body)
env['form_context_depth'] -= 1
if env['form_context_depth'] == 0
# Only emit the JS templates at the top-level
s << templates_for_js(values_from["jsonmodel_type"])
end
s << "</div>".html_safe
s
end
def templates
@templates ||= {}
@templates
end
class BaseDefinition
def required?(name)
false
end
end
def jsonmodel_definition(type, root = nil)
JSONModelDefinition.new(JSONModel(type), root)
end
class JSONModelDefinition < BaseDefinition
def initialize(jsonmodel, root)
@jsonmodel = jsonmodel
@root = root
end
def required?(name)
(jsonmodel_schema_definition(name) &&
jsonmodel_schema_definition(name)['ifmissing'] === 'error')
end
def default_for(name)
if jsonmodel_schema_definition(name)
if jsonmodel_schema_definition(name).has_key?('dynamic_enum')
if jsonmodel_schema_definition(name)['default']
Rails.logger.warn("Superfluous default value at: #{@jsonmodel}.#{name} ")
end
JSONModel.enum_default_value(jsonmodel_schema_definition(name)['dynamic_enum'])
else
jsonmodel_schema_definition(name)['default']
end
else
nil
end
end
def allowable_types_for(name)
defn = jsonmodel_schema_definition(name)
if defn
ASUtils.extract_nested_strings(defn).map {|s|
ref = JSONModel.parse_jsonmodel_ref(s)
ref.first.to_s if ref
}.compact
else
[]
end
end
def options_for(context, property, add_empty_options = false, opts = {})
options = []
options.push([(opts[:empty_label] || ""),""]) if add_empty_options
defn = jsonmodel_schema_definition(property)
jsonmodel_enum_for(property).each do |v|
if opts[:include] && !opts[:include].include?(v)
next
end
if opts[:exclude] && opts[:exclude].include?(v)
next
end
if opts.has_key?(:i18n_path_for) && opts[:i18n_path_for].has_key?(v)
i18n_path = opts[:i18n_path_for][v]
elsif opts.has_key?(:i18n_prefix)
i18n_path = "#{opts[:i18n_prefix]}.#{v}"
elsif defn.has_key?('dynamic_enum')
i18n_path = {
:enumeration => defn['dynamic_enum'],
:value => v
}
else
i18n_path = context.i18n_for("#{Array(property).last}_#{v}")
end
options.push([I18n.t(i18n_path, :default => v), v])
end
options.sort {|a,b| a[0] <=> b[0]}
end
private
def jsonmodel_enum_for(property)
defn = jsonmodel_schema_definition(property)
if defn["enum"]
defn["enum"]
elsif defn["dynamic_enum"]
JSONModel.enum_values(defn['dynamic_enum'])
else
raise "No enum found for #{property}"
end
end
def jsonmodel_schema_definition(property)
schema = @jsonmodel.schema
properties = Array(property).clone
if @root
properties = [@root] + properties
end
while !properties.empty?
if schema['type'] == 'object'
schema = schema['properties']
elsif schema['type'] == 'array'
schema = schema['items']
else
property = properties.shift
if properties.empty?
return schema[property]
else
schema = schema[property]
end
end
end
nil
end
end
def define_template(name, definition = nil, &block)
@templates ||= {}
@templates[name] = {
:block => block,
:definition => (definition || BaseDefinition.new),
}
end
def templates_for_js(jsonmodel_type = nil)
result = ""
return result if @templates.blank?
obj = {}
obj['jsonmodel_type'] = jsonmodel_type if jsonmodel_type
@templates.each do |name, template|
context = FormContext.new("${path}", obj, self)
def context.id_for(name, qualify = true)
name = path(name) if qualify
name.gsub(/[\[\]]/, '_').gsub('${path}', '${id_path}')
end
context.instance_eval do
@active_template = name
end
result << "<div id=\"template_#{name}\"><!--"
result << capture(context, &template[:block])
result << "--></div>"
end
result.html_safe
end
def readonly_context(name, values_from = {}, &body)
context = ReadOnlyContext.new(name, values_from, self)
# Not feeling great about this, but we render the form twice: the first pass
# sets up the mapping from form input names to i18n keys, while the second
# actually uses that map to set the labels correctly.
capture(context, &body)
s = "<div class=\"readonly-context form-horizontal\">".html_safe
s << capture(context, &body)
s << "</div>".html_safe
s
end
PROPERTIES_TO_EXCLUDE_FROM_READ_ONLY_VIEW = ["jsonmodel_type", "lock_version", "_resolved", "uri", "ref", "create_time", "system_mtime", "user_mtime", "created_by", "last_modified_by", "sort_name_auto_generate", "suppressed", "display_string", "file_uri"]
def read_only_view(hash, opts = {})
jsonmodel_type = hash["jsonmodel_type"]
schema = JSONModel(jsonmodel_type).schema
prefix = opts[:plugin] ? 'plugins.' : ''
html = "<div class='form-horizontal'>"
hash.reject {|k,v| PROPERTIES_TO_EXCLUDE_FROM_READ_ONLY_VIEW.include?(k)}.each do |property, value|
if schema and schema["properties"].has_key?(property)
if (schema["properties"][property].has_key?('dynamic_enum'))
value = I18n.t("#{prefix}enumerations.#{schema["properties"][property]["dynamic_enum"]}.#{value}", :default => value)
elsif schema["properties"][property].has_key?("enum")
value = I18n.t("#{prefix}#{jsonmodel_type.to_s}.#{property}_#{value}", :default => value)
elsif schema["properties"][property]["type"] === "boolean"
value = value === true ? "True" : "False"
elsif schema["properties"][property]["type"] === "date"
value = value.blank? ? "" : Date.strptime(value, "%Y-%m-%d")
elsif schema["properties"][property]["type"] === "array"
# this view doesn't support arrays
next
elsif value.kind_of? Hash
# can't display an object either
next
end
end
html << "<div class='form-group'>"
html << "<div class='control-label col-md-3'>#{I18n.t("#{prefix}#{jsonmodel_type.to_s}.#{property}")}</div>"
html << "<div class='label-only col-md-9'>#{value}</div>"
html << "</div>"
end
html << "</div>"
html.html_safe
end
def preserve_newlines(string)
string.gsub(/\n/, '<br>')
end
def update_monitor_params(record)
{
:"data-update-monitor" => true,
:"data-update-monitor-url" => url_for(:controller => :update_monitor, :action => :poll),
:"data-update-monitor-record-uri" => record.uri,
:"data-update-monitor-record-is-stale" => !!@record_is_stale,
:"data-update-monitor-lock_version" => record.lock_version
}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment