presenting 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|