presenting 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README +10 -0
- data/Rakefile +22 -0
- data/app/assets/javascript/grid.js +0 -0
- data/app/assets/javascript/search.js +13 -0
- data/app/assets/stylesheets/details-color.css +7 -0
- data/app/assets/stylesheets/details.css +10 -0
- data/app/assets/stylesheets/form.css +1 -0
- data/app/assets/stylesheets/grid-color.css +71 -0
- data/app/assets/stylesheets/grid.css +64 -0
- data/app/assets/stylesheets/search-color.css +16 -0
- data/app/assets/stylesheets/search.css +45 -0
- data/app/controllers/presentation/assets_controller.rb +42 -0
- data/app/views/presentations/_details.erb +11 -0
- data/app/views/presentations/_field_search.erb +14 -0
- data/app/views/presentations/_form.erb +20 -0
- data/app/views/presentations/_grid.erb +70 -0
- data/app/views/presentations/_search.erb +7 -0
- data/config/routes.rb +3 -0
- data/lib/presentation/base.rb +48 -0
- data/lib/presentation/details.rb +19 -0
- data/lib/presentation/field_search.rb +65 -0
- data/lib/presentation/form.rb +149 -0
- data/lib/presentation/grid.rb +160 -0
- data/lib/presentation/search.rb +9 -0
- data/lib/presenting/attribute.rb +51 -0
- data/lib/presenting/configurable.rb +10 -0
- data/lib/presenting/defaults.rb +10 -0
- data/lib/presenting/field_set.rb +26 -0
- data/lib/presenting/form_helpers.rb +51 -0
- data/lib/presenting/helpers.rb +110 -0
- data/lib/presenting/sanitize.rb +19 -0
- data/lib/presenting/search.rb +185 -0
- data/lib/presenting/sorting.rb +87 -0
- data/lib/presenting/view.rb +3 -0
- data/lib/presenting.rb +9 -0
- data/rails/init.rb +12 -0
- data/test/assets_test.rb +58 -0
- data/test/attribute_test.rb +61 -0
- data/test/configurable_test.rb +20 -0
- data/test/details_test.rb +68 -0
- data/test/field_search_test.rb +102 -0
- data/test/field_set_test.rb +46 -0
- data/test/form_test.rb +287 -0
- data/test/grid_test.rb +219 -0
- data/test/helpers_test.rb +72 -0
- data/test/presenting_test.rb +15 -0
- data/test/rails/app/controllers/application_controller.rb +15 -0
- data/test/rails/app/controllers/users_controller.rb +36 -0
- data/test/rails/app/helpers/application_helper.rb +3 -0
- data/test/rails/app/helpers/users_helper.rb +2 -0
- data/test/rails/app/models/user.rb +2 -0
- data/test/rails/app/views/layouts/application.html.erb +15 -0
- data/test/rails/app/views/users/index.html.erb +10 -0
- data/test/rails/app/views/users/new.html.erb +2 -0
- data/test/rails/app/views/users/show.html.erb +1 -0
- data/test/rails/config/boot.rb +109 -0
- data/test/rails/config/database.yml +17 -0
- data/test/rails/config/environment.rb +13 -0
- data/test/rails/config/environments/development.rb +17 -0
- data/test/rails/config/environments/production.rb +24 -0
- data/test/rails/config/environments/test.rb +22 -0
- data/test/rails/config/locales/en.yml +5 -0
- data/test/rails/config/routes.rb +5 -0
- data/test/rails/db/development.sqlite3 +0 -0
- data/test/rails/db/migrate/20090213085444_create_users.rb +13 -0
- data/test/rails/db/migrate/20090213085607_populate_users.rb +13 -0
- data/test/rails/db/schema.rb +23 -0
- data/test/rails/db/test.sqlite3 +0 -0
- data/test/rails/log/development.log +858 -0
- data/test/rails/public/404.html +30 -0
- data/test/rails/public/422.html +30 -0
- data/test/rails/public/500.html +33 -0
- data/test/rails/public/javascripts/application.js +2 -0
- data/test/rails/public/javascripts/jquery.livequery.min.js +11 -0
- data/test/rails/public/javascripts/prototype.js +4320 -0
- data/test/rails/script/console +3 -0
- data/test/rails/script/dbconsole +3 -0
- data/test/rails/script/destroy +3 -0
- data/test/rails/script/generate +3 -0
- data/test/rails/script/plugin +3 -0
- data/test/rails/script/runner +3 -0
- data/test/rails/script/server +3 -0
- data/test/sanitize_test.rb +15 -0
- data/test/search_conditions_test.rb +137 -0
- data/test/search_test.rb +30 -0
- data/test/sorting_test.rb +63 -0
- data/test/test_helper.rb +66 -0
- metadata +217 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
module Presentation
|
2
|
+
class Form < Base
|
3
|
+
# TODO
|
4
|
+
# field type extra details?
|
5
|
+
# * text
|
6
|
+
# * text_area
|
7
|
+
# * password
|
8
|
+
# * check_box checked/unchecked values
|
9
|
+
# * radio (= dropdown) options
|
10
|
+
# * dropdown (= radio) options
|
11
|
+
# * multi-select options
|
12
|
+
# * recordselect url?
|
13
|
+
# * calendar constraints
|
14
|
+
# * time constraints
|
15
|
+
# * date
|
16
|
+
# * datetime
|
17
|
+
#
|
18
|
+
# other
|
19
|
+
# - example / description / help text
|
20
|
+
# - nested fields
|
21
|
+
|
22
|
+
# Fields may be grouped. Groups may or may not have names. Here's how:
|
23
|
+
#
|
24
|
+
# Presentation::Form.new(:groups => [
|
25
|
+
# [:a, :b], # creates a nameless group with fields :a and :b
|
26
|
+
# {"foo" => [:c, :d]} # creates a group named "foo" with fields :c and :d
|
27
|
+
# ])
|
28
|
+
#
|
29
|
+
# Note that if you don't need groups it'll be simpler to just use fields= instead.
|
30
|
+
def groups
|
31
|
+
@groups ||= GroupSet.new
|
32
|
+
end
|
33
|
+
def groups=(args)
|
34
|
+
args.each do |group|
|
35
|
+
groups << group
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class GroupSet < Array
|
40
|
+
def <<(val)
|
41
|
+
if val.is_a? Hash
|
42
|
+
opts = {:name => val.keys.first, :fields => val.values.first}
|
43
|
+
else
|
44
|
+
opts = {:fields => val}
|
45
|
+
end
|
46
|
+
super Group.new(opts)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Group
|
51
|
+
include Presenting::Configurable
|
52
|
+
|
53
|
+
# a completely optional group name
|
54
|
+
attr_accessor :name
|
55
|
+
|
56
|
+
# the fields in the group
|
57
|
+
def fields
|
58
|
+
@fields ||= Presenting::FieldSet.new(Field, :name, :type)
|
59
|
+
end
|
60
|
+
def fields=(args)
|
61
|
+
args.each do |field|
|
62
|
+
fields << field
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Used to define fields in a group-less form.
|
68
|
+
def fields
|
69
|
+
if groups.empty?
|
70
|
+
groups << []
|
71
|
+
end
|
72
|
+
groups.first.fields
|
73
|
+
end
|
74
|
+
def fields=(args)
|
75
|
+
args.each do |field|
|
76
|
+
fields << field
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# The url where the form posts. May be anything that url_for accepts, including
|
81
|
+
# a set of records.
|
82
|
+
def url
|
83
|
+
@url ||= presentable
|
84
|
+
end
|
85
|
+
attr_writer :url
|
86
|
+
|
87
|
+
# What method the form should use to post. Should default intelligently enough from
|
88
|
+
# the presentable. Not sure what use case would require it being set manually.
|
89
|
+
def method
|
90
|
+
@method ||= presentable.new_record? ? :post : :put
|
91
|
+
end
|
92
|
+
attr_writer :method
|
93
|
+
|
94
|
+
# the text on the submit button
|
95
|
+
def button
|
96
|
+
@button ||= presentable.new_record? ? 'Create' : 'Update'
|
97
|
+
end
|
98
|
+
attr_writer :button
|
99
|
+
|
100
|
+
# a passthrough for form_for's html. useful for classifying a form for ajax behavior (e.g. :html => {:class => 'ajax'})
|
101
|
+
attr_accessor :html
|
102
|
+
|
103
|
+
class Field
|
104
|
+
include Presenting::Configurable
|
105
|
+
|
106
|
+
# the display label of the field
|
107
|
+
def label
|
108
|
+
@label ||= name.to_s.titleize
|
109
|
+
end
|
110
|
+
attr_writer :label
|
111
|
+
|
112
|
+
# the parameter name of the field
|
113
|
+
attr_accessor :name
|
114
|
+
|
115
|
+
# where the value for this field comes from.
|
116
|
+
# - String: a fixed value
|
117
|
+
# - Symbol: a method on the record (no arguments)
|
118
|
+
# - Proc: a custom block that accepts the record as an argument
|
119
|
+
def value
|
120
|
+
@value ||= name.to_sym
|
121
|
+
end
|
122
|
+
attr_writer :value
|
123
|
+
|
124
|
+
def value_from(obj) #:nodoc:
|
125
|
+
v = case value
|
126
|
+
when Symbol: obj.send(value)
|
127
|
+
when String: value
|
128
|
+
when Proc: value.call(obj)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# the widget type for the field. use type_options to pass arguments to the widget.
|
133
|
+
def type
|
134
|
+
@type ||= :string
|
135
|
+
end
|
136
|
+
attr_writer :type
|
137
|
+
|
138
|
+
# unrestricted options storage for the widget type. this could be a list of options for a select, or extra configuration for a calendar widget.
|
139
|
+
attr_accessor :type_options
|
140
|
+
end
|
141
|
+
|
142
|
+
def iname; :form end
|
143
|
+
|
144
|
+
delegate :request_forgery_protection_token, :allow_forgery_protection, :to => :controller
|
145
|
+
def protect_against_forgery? #:nodoc:
|
146
|
+
allow_forgery_protection && request_forgery_protection_token
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module Presentation
|
2
|
+
# TODO: ability to render a hash
|
3
|
+
# TODO: custom css classes for rows and/or cells
|
4
|
+
# TODO: document or complain for required options -- id and fields
|
5
|
+
# TODO: make fields= accept an ActiveRecord::Base.columns array for a default field set
|
6
|
+
class Grid < Base
|
7
|
+
# The id for this presentation. Required.
|
8
|
+
attr_accessor :id
|
9
|
+
|
10
|
+
# The display title for this presentation. Will default based on the id.
|
11
|
+
attr_writer :title
|
12
|
+
def title
|
13
|
+
@title ||= self.id.titleize
|
14
|
+
end
|
15
|
+
|
16
|
+
# Paradigm Example:
|
17
|
+
# Grid.new(:fields => [
|
18
|
+
# :email,
|
19
|
+
# {"Full Name" => proc{|r| [r.first_name, r.last_name].join(' ')}},
|
20
|
+
# {"Roles" => {:value => :roles, :type => :collection}}
|
21
|
+
# ])
|
22
|
+
#
|
23
|
+
# Is equivalent to:
|
24
|
+
# g = Grid.new
|
25
|
+
# g.fields << :email
|
26
|
+
# g.fields << {"Full Name" => proc{|r| [r.first_name, r.last_name].join(' ')},
|
27
|
+
# g.fields << {"Roles" => {:value => :roles, :type => :collection}}
|
28
|
+
def fields=(args)
|
29
|
+
args.each do |field|
|
30
|
+
self.fields << field
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def fields
|
35
|
+
@fields ||= Presenting::FieldSet.new(Field, :name, :value)
|
36
|
+
end
|
37
|
+
|
38
|
+
def colspan
|
39
|
+
@colspan ||= fields.size + (record_links.empty? ? 0 : 1)
|
40
|
+
end
|
41
|
+
|
42
|
+
def iname; :grid end
|
43
|
+
|
44
|
+
class Field < Presenting::Attribute
|
45
|
+
# Defines how this field sorts. This means two things:
|
46
|
+
# 1. whether it sorts
|
47
|
+
# 2. what name it uses to sort
|
48
|
+
#
|
49
|
+
# Examples:
|
50
|
+
#
|
51
|
+
# # The field is sortable and assumes the sort_name of "first_name".
|
52
|
+
# # This is the default.
|
53
|
+
# Field.new(:sortable => true, :name => "First Name")
|
54
|
+
#
|
55
|
+
# # The field is sortable and assumes the sort_name of "first".
|
56
|
+
# Field.new(:sortable => 'first', :name => 'First Name')
|
57
|
+
#
|
58
|
+
# # The field is unsortable.
|
59
|
+
# Field.new(:sortable => false)
|
60
|
+
def sortable=(val)
|
61
|
+
@sort_name = case val
|
62
|
+
when TrueClass: self.id
|
63
|
+
when FalseClass, NilClass: nil
|
64
|
+
else val.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# if the field is sortable at all
|
69
|
+
def sortable?
|
70
|
+
self.sortable = Presenting::Defaults.grid_is_sortable unless defined? @sort_name
|
71
|
+
!@sort_name.blank?
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :sort_name
|
75
|
+
|
76
|
+
# is this field sorted in the given request?
|
77
|
+
def is_sorted?(request)
|
78
|
+
@is_sorted ||= if sortable? and sorting = request.query_parameters["sort"] and sorting[sort_name]
|
79
|
+
sorting[sort_name].to_s.match(/desc/i) ? 'desc' : 'asc'
|
80
|
+
else
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# for the view -- modifies the current request such that it would sort this field.
|
86
|
+
def sorted_url(request)
|
87
|
+
if current_direction = is_sorted?(request)
|
88
|
+
next_direction = current_direction == 'desc' ? 'asc' : 'desc'
|
89
|
+
else
|
90
|
+
next_direction = 'desc'
|
91
|
+
end
|
92
|
+
request.path + '?' + request.query_parameters.merge("sort" => {sort_name => next_direction}).to_param
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
## Planned
|
97
|
+
##
|
98
|
+
|
99
|
+
# TODO: discover "type" from data class (ActiveRecord) if available
|
100
|
+
# TODO: decorate a Hash object so type is specifiable there as well
|
101
|
+
# PLAN: type should determine how a field renders. custom types for custom renders. this should be the second option to present().
|
102
|
+
# attr_accessor :type
|
103
|
+
|
104
|
+
# PLAN: a field's description would appear in the header column, perhaps only visibly in a tooltip
|
105
|
+
# attr_accessor :description
|
106
|
+
|
107
|
+
# PLAN: any field may be linked. this would happen after :value and :type.
|
108
|
+
# attr_accessor :link
|
109
|
+
end
|
110
|
+
|
111
|
+
# Links are an area where I almost made the mistake of too much configuration. Presentations are configured in the view,
|
112
|
+
# and all of the view helpers are available. When I looked at the (simple) configuration I was building and realized that
|
113
|
+
# I could just as easily take the result of link_to, well, I felt a little silly.
|
114
|
+
#
|
115
|
+
# Compare:
|
116
|
+
#
|
117
|
+
# @grid.links = [
|
118
|
+
# {:name => 'Foo', :url => foo_path, :class => 'foo'}
|
119
|
+
# ]
|
120
|
+
#
|
121
|
+
# vs:
|
122
|
+
#
|
123
|
+
# @grid.links = [
|
124
|
+
# link_to('Foo', foo_path, :class => 'foo')
|
125
|
+
# ]
|
126
|
+
#
|
127
|
+
# Not only is the second example (the supported example, by the way) shorter and cleaner, it encourages the developer
|
128
|
+
# to stay in touch with the Rails internals and therefore discourages a configuration-heavy mindset.
|
129
|
+
def links=(set)
|
130
|
+
set.compact.each do |link|
|
131
|
+
raise ArgumentError, "Links must be strings, such as the output of link_to()." unless link.is_a?(String)
|
132
|
+
links << link
|
133
|
+
end
|
134
|
+
end
|
135
|
+
def links
|
136
|
+
@links ||= []
|
137
|
+
end
|
138
|
+
|
139
|
+
# Like links, except the link will appear for each record. This means that the link must be a block that accepts the
|
140
|
+
# record as its argument. For example:
|
141
|
+
#
|
142
|
+
# @grid.record_links = [
|
143
|
+
# proc{|record| link_to("Foo", foo_path(record), :class => 'foo') }
|
144
|
+
# ]
|
145
|
+
#
|
146
|
+
def record_links=(set)
|
147
|
+
set.compact.each do |link|
|
148
|
+
raise ArgumentError, "Record links must be blocks that accept the record as an argument." unless link.respond_to?(:call) and link.arity == 1
|
149
|
+
record_links << link
|
150
|
+
end
|
151
|
+
end
|
152
|
+
def record_links
|
153
|
+
@record_links ||= []
|
154
|
+
end
|
155
|
+
|
156
|
+
def paginate?
|
157
|
+
defined? WillPaginate and presentable.is_a?(WillPaginate::Collection)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Presenting
|
2
|
+
# represents an attribute meant to be read from a record
|
3
|
+
# used for things like Grid and Details.
|
4
|
+
# not intended for things like Form or FieldSearch
|
5
|
+
class Attribute
|
6
|
+
include Presenting::Configurable
|
7
|
+
|
8
|
+
def name=(val)
|
9
|
+
self.value ||= val # don't lazy define :value, because we're about to typecast here
|
10
|
+
if val.is_a? Symbol
|
11
|
+
@name = val.to_s.titleize
|
12
|
+
else
|
13
|
+
@name = val.to_s
|
14
|
+
end
|
15
|
+
end
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
# The short programmatic name for this field. Can be used as a CSS class, sorting name, etc.
|
19
|
+
def id=(val)
|
20
|
+
@id = val.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def id
|
24
|
+
@id ||= name.to_s.underscore.gsub(/[^a-z0-9]/i, '_').gsub(/__+/, '_').sub(/_$/, '')
|
25
|
+
end
|
26
|
+
|
27
|
+
# Where a field's value comes from. Depends heavily on the data type you provide.
|
28
|
+
# - String: fixed value (as provided)
|
29
|
+
# - Symbol: a method on the record (no arguments)
|
30
|
+
# - Proc: a custom block that accepts the record as an argument
|
31
|
+
attr_accessor :value
|
32
|
+
|
33
|
+
def value_from(obj) #:nodoc:
|
34
|
+
case value
|
35
|
+
when Symbol: obj.is_a?(Hash) ? obj[value] : obj.send(value)
|
36
|
+
when String: value
|
37
|
+
when Proc: value.call(obj)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# whether html should be sanitize. right now this actually means html escaping.
|
42
|
+
# consider: by default, do not sanitize if value is a String?
|
43
|
+
attr_writer :sanitize
|
44
|
+
def sanitize?
|
45
|
+
unless defined? @sanitize
|
46
|
+
@sanitize = Presenting::Defaults.sanitize_fields
|
47
|
+
end
|
48
|
+
@sanitize
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Presenting::Defaults
|
2
|
+
# Whether the columns in the Grid should be sortable by default
|
3
|
+
mattr_accessor :grid_is_sortable
|
4
|
+
self.grid_is_sortable = true
|
5
|
+
|
6
|
+
# Whether fields should be sanitized by default
|
7
|
+
mattr_accessor :sanitize_fields
|
8
|
+
self.sanitize_fields = true
|
9
|
+
end
|
10
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Presenting::FieldSet < Array
|
2
|
+
def initialize(field_class, primary, secondary)
|
3
|
+
@klass = field_class
|
4
|
+
@primary_attribute = primary
|
5
|
+
@secondary_attribute = secondary
|
6
|
+
end
|
7
|
+
|
8
|
+
def <<(field)
|
9
|
+
if field.is_a? Hash
|
10
|
+
k, v = *field.to_a.first
|
11
|
+
opts = v.is_a?(Hash) ? v : {@secondary_attribute => v}
|
12
|
+
opts[@primary_attribute] = k
|
13
|
+
else
|
14
|
+
opts = {@primary_attribute => field}
|
15
|
+
end
|
16
|
+
super @klass.new(opts)
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](key)
|
20
|
+
detect{|i| i.send(@primary_attribute) == key}
|
21
|
+
end
|
22
|
+
|
23
|
+
def []=(key, val)
|
24
|
+
self << {key => val}
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Presenting::FormHelpers
|
2
|
+
def present(field)
|
3
|
+
send("present_#{field.type}_input", field)
|
4
|
+
end
|
5
|
+
|
6
|
+
def present_readonly_input(field)
|
7
|
+
text_field field.name, :disabled => true, :value => field.value_from(object)
|
8
|
+
end
|
9
|
+
|
10
|
+
def present_string_input(field)
|
11
|
+
text_field field.name, :value => field.value_from(object)
|
12
|
+
end
|
13
|
+
|
14
|
+
def present_hidden_input(field)
|
15
|
+
hidden_field field.name, :value => field.value_from(object)
|
16
|
+
end
|
17
|
+
|
18
|
+
def present_text_input(field)
|
19
|
+
text_area field.name, :value => field.value_from(object)
|
20
|
+
end
|
21
|
+
|
22
|
+
def present_password_input(field)
|
23
|
+
password_field field.name
|
24
|
+
end
|
25
|
+
|
26
|
+
def present_boolean_input(field)
|
27
|
+
check_box field.name
|
28
|
+
end
|
29
|
+
|
30
|
+
def present_dropdown_input(field)
|
31
|
+
view.select_tag "#{object_name}[#{field.name}]", view.options_for_select(field.type_options, object.send(field.name))
|
32
|
+
end
|
33
|
+
alias_method :present_select_input, :present_dropdown_input
|
34
|
+
|
35
|
+
def present_multi_select_input(field)
|
36
|
+
view.select_tag "#{object_name}[#{field.name}][]", view.options_for_select(field.type_options, object.send(field.name)), :multiple => true
|
37
|
+
end
|
38
|
+
|
39
|
+
def present_radios_input(field)
|
40
|
+
field.type_options.collect do |(display, value)|
|
41
|
+
label("#{field.name}_#{value}", display) +
|
42
|
+
radio_button(field.name, value)
|
43
|
+
end.join
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def view
|
49
|
+
@template
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Presenting
|
2
|
+
module Helpers
|
3
|
+
def presentation_stylesheets(*args)
|
4
|
+
stylesheet_link_tag presentation_stylesheet_path(args.sort.join(','))
|
5
|
+
end
|
6
|
+
|
7
|
+
def presentation_javascript(*args)
|
8
|
+
javascript_include_tag presentation_javascript_path(args.sort.join(','))
|
9
|
+
end
|
10
|
+
|
11
|
+
def present(*args, &block)
|
12
|
+
options = args.length > 1 ? args.extract_options! : {}
|
13
|
+
|
14
|
+
if args.first.is_a? Symbol
|
15
|
+
object, presentation = nil, args.first
|
16
|
+
else
|
17
|
+
object, presentation = args.first, args.second
|
18
|
+
end
|
19
|
+
|
20
|
+
if presentation
|
21
|
+
klass = "Presentation::#{presentation.to_s.camelcase}".constantize rescue nil
|
22
|
+
if klass
|
23
|
+
instance = klass.new(options, &block)
|
24
|
+
instance.presentable = object
|
25
|
+
instance.controller = controller
|
26
|
+
instance.render
|
27
|
+
elsif respond_to?(method_name = "present_#{presentation}")
|
28
|
+
send(method_name, object, options)
|
29
|
+
else
|
30
|
+
raise ArgumentError, "unknown presentation `#{presentation}'"
|
31
|
+
end
|
32
|
+
elsif object.respond_to?(:loaded?) # AssociationProxy
|
33
|
+
present_association(object, options)
|
34
|
+
else
|
35
|
+
present_by_class(object, options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# presents a text search widget for the given field (a Presentation::FieldSearch::Field, probably)
|
40
|
+
def present_text_search(field, options = {})
|
41
|
+
current_value = (params[:search][field.param][:value] rescue nil)
|
42
|
+
text_field_tag options[:name], h(current_value)
|
43
|
+
end
|
44
|
+
|
45
|
+
# presents a checkbox search widget for the given field (a Presentation::FieldSearch::Field, probably)
|
46
|
+
def present_checkbox_search(field, options = {})
|
47
|
+
current_value = (params[:search][field.param][:value] rescue nil)
|
48
|
+
check_box_tag options[:name], '1', current_value.to_s == '1'
|
49
|
+
end
|
50
|
+
|
51
|
+
# presents a dropdown/select search widget for the given field (a Presentation::FieldSearch::Field, probably)
|
52
|
+
def present_dropdown_search(field, options = {})
|
53
|
+
current_value = (params[:search][field.param][:value] rescue nil)
|
54
|
+
select_tag options[:name], options_for_select(normalize_dropdown_options_to_strings(field.options), current_value)
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
# We want to normalize the value elements of the dropdown options to strings so that
|
60
|
+
# they will match against params[:search].
|
61
|
+
#
|
62
|
+
# Need to handle the three different dropdown options formats:
|
63
|
+
# * array of strings
|
64
|
+
# * array of arrays
|
65
|
+
# * hash
|
66
|
+
def normalize_dropdown_options_to_strings(options)
|
67
|
+
options.to_a.map do |element|
|
68
|
+
if element.is_a? String
|
69
|
+
[element, element]
|
70
|
+
else
|
71
|
+
[element.first, element.last.to_s]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# TODO: special handling for associations (displaying activerecords)
|
77
|
+
def present_association(object, options = {})
|
78
|
+
present_by_class(object, options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def present_by_class(object, options = {})
|
82
|
+
case object
|
83
|
+
when Array
|
84
|
+
content_tag "ol" do
|
85
|
+
object.collect do |i|
|
86
|
+
content_tag "li", present(i, options)
|
87
|
+
end.join.html_safe
|
88
|
+
end
|
89
|
+
|
90
|
+
when Hash
|
91
|
+
# sort by keys
|
92
|
+
content_tag "dl" do
|
93
|
+
object.keys.sort.collect do |k|
|
94
|
+
content_tag("dt", k) +
|
95
|
+
content_tag("dd", present(object[k], options))
|
96
|
+
end.join.html_safe
|
97
|
+
end
|
98
|
+
|
99
|
+
when TrueClass, FalseClass
|
100
|
+
object ? "True" : "False"
|
101
|
+
|
102
|
+
when Date, Time, DateTime
|
103
|
+
l(object, :format => :default)
|
104
|
+
|
105
|
+
else
|
106
|
+
options[:h] ? h(object.to_s) : object.to_s
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Presenting::Sanitize
|
2
|
+
class << self
|
3
|
+
include ERB::Util
|
4
|
+
|
5
|
+
# escape but preserve Arrays and Hashes
|
6
|
+
def h(val)
|
7
|
+
case val
|
8
|
+
when Array
|
9
|
+
val.map{|i| h(i)}
|
10
|
+
|
11
|
+
when Hash
|
12
|
+
val.clone.each{|k, v| val[h(k)] = h(v)}
|
13
|
+
|
14
|
+
else
|
15
|
+
html_escape(val)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|