padrino-fields 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.document +5 -0
  2. data/Gemfile +40 -0
  3. data/Gemfile.lock +133 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.markdown +189 -0
  6. data/Rakefile +53 -0
  7. data/VERSION +1 -0
  8. data/lib/padrino-fields.rb +30 -0
  9. data/lib/padrino-fields/form_builder.rb +183 -0
  10. data/lib/padrino-fields/form_helpers.rb +63 -0
  11. data/lib/padrino-fields/orms/datamapper.rb +54 -0
  12. data/lib/padrino-fields/settings.rb +18 -0
  13. data/load_paths.rb +12 -0
  14. data/padrino-fields.gemspec +141 -0
  15. data/test/.DS_Store +0 -0
  16. data/test/fixtures/datamapper/app.rb +48 -0
  17. data/test/fixtures/datamapper/views/capture_concat.erb +14 -0
  18. data/test/fixtures/datamapper/views/capture_concat.haml +12 -0
  19. data/test/fixtures/datamapper/views/content_for.erb +11 -0
  20. data/test/fixtures/datamapper/views/content_for.haml +9 -0
  21. data/test/fixtures/datamapper/views/content_tag.erb +11 -0
  22. data/test/fixtures/datamapper/views/content_tag.haml +9 -0
  23. data/test/fixtures/datamapper/views/fields_for.erb +20 -0
  24. data/test/fixtures/datamapper/views/fields_for.haml +15 -0
  25. data/test/fixtures/datamapper/views/form_for.erb +56 -0
  26. data/test/fixtures/datamapper/views/form_for.haml +47 -0
  27. data/test/fixtures/datamapper/views/form_tag.erb +56 -0
  28. data/test/fixtures/datamapper/views/form_tag.haml +45 -0
  29. data/test/fixtures/datamapper/views/link_to.erb +5 -0
  30. data/test/fixtures/datamapper/views/link_to.haml +4 -0
  31. data/test/fixtures/datamapper/views/mail_to.erb +3 -0
  32. data/test/fixtures/datamapper/views/mail_to.haml +3 -0
  33. data/test/fixtures/datamapper/views/meta_tag.erb +3 -0
  34. data/test/fixtures/datamapper/views/meta_tag.haml +3 -0
  35. data/test/helper.rb +128 -0
  36. data/test/test_datamapper.rb +109 -0
  37. data/test/test_form_builder.rb +193 -0
  38. data/test/test_form_helpers.rb +59 -0
  39. data/test/test_settings.rb +51 -0
  40. metadata +288 -0
@@ -0,0 +1,30 @@
1
+ require 'sinatra'
2
+ require 'padrino'
3
+ require 'padrino-core/support_lite' unless defined?(SupportLite)
4
+ require 'cgi'
5
+ require 'i18n'
6
+ require 'enumerator'
7
+
8
+ FileSet.glob_require('padrino-fields/**/*.rb', __FILE__)
9
+
10
+ # Load our locales
11
+ I18n.load_path += Dir["#{File.dirname(__FILE__)}/padrino-helpers/locale/*.yml"]
12
+
13
+ module PadrinoFields
14
+ ##
15
+ # This component provides view helpers and shortcuts for generating form fields
16
+ # Should feel familiar to users of Formtastic or SimpleForm
17
+
18
+ ##
19
+ # Register Padrino::Helpers::Fields for Padrino::Application
20
+ #
21
+ class << self
22
+ def registered(app)
23
+ app.set :default_builder, 'PadrinoFieldsBuilder'
24
+ app.helpers Padrino::Helpers::FormBuilder::PadrinoFieldsBuilder
25
+ rescue
26
+ end
27
+ alias :included :registered
28
+ end
29
+
30
+ end # PadrinoFields
@@ -0,0 +1,183 @@
1
+ require 'padrino-helpers'
2
+ require File.expand_path(File.dirname(__FILE__) + '/form_helpers')
3
+ require File.expand_path(File.dirname(__FILE__) + '/orms/datamapper') if defined?(DataMapper)
4
+ require File.expand_path(File.dirname(__FILE__) + '/settings')
5
+
6
+ module Padrino
7
+ module Helpers
8
+ module FormBuilder #:nodoc:
9
+ class PadrinoFieldsBuilder < AbstractFormBuilder #:nodoc:
10
+
11
+ include PadrinoFields::Settings
12
+ include PadrinoFields::DataMapperWrapper if defined?(DataMapper)
13
+
14
+ @@settings = PadrinoFields::Settings
15
+
16
+ attr_reader :template, :object_name, :object
17
+
18
+ def input(attribute, options={})
19
+ options.reverse_merge!(:caption => options.delete(:caption)) if options[:caption]
20
+ type = options[:as] || klazz.form_column_type_for(attribute)
21
+ field_html = ""
22
+ if type == :boolean || options[:as] == :boolean
23
+ field_html << default_input(attribute,type,options)
24
+ field_html << setup_label(attribute,type,labelize(options))
25
+ else
26
+ field_html << setup_label(attribute,type,labelize(options))
27
+ field_html << default_input(attribute,type, options)
28
+ end
29
+ field_html << hint(options[:hint]) if options[:hint]
30
+ field_html << @template.error_message_on(@object,attribute,{}) if @object.errors.any?
31
+ @template.content_tag(@@settings.container, :class => css_class(attribute,type,options[:disabled])) do
32
+ field_html
33
+ end
34
+ end
35
+
36
+ def default_input(attribute,type,options={})
37
+ input_options = options.keep_if {|key, value| key != :as}
38
+ if options[:options] || options[:grouped_options]
39
+ if type==:radios || type == :checks
40
+ collect_inputs_as(attribute,type,input_options)
41
+ else
42
+ select(attribute,input_options)
43
+ end
44
+ else
45
+ singular_input_for(attribute,type,options)
46
+ end
47
+ end
48
+
49
+ %w(date email number search tel url).each do |type|
50
+ class_eval <<-EOF
51
+ ##
52
+ # Constructs a #{type} field input from the given options
53
+ #
54
+ # ==== Examples
55
+ #
56
+ # #{type}_field :username, :class => 'long'
57
+ #
58
+ def #{type}_field(field, options={})
59
+ options.reverse_merge!(:value => field_value(field), :id => field_id(field))
60
+ options.merge!(:class => field_error(field, options))
61
+ @template.#{type}_field_tag(field_name(field), options)
62
+ end
63
+ EOF
64
+ end
65
+
66
+
67
+ def collect_inputs_as(attribute,type,options={})
68
+ options[:options].map do |item|
69
+ collection_input(attribute,type,item,options.keep_if {|key, value| key != :options})
70
+ end.join("")
71
+ end
72
+
73
+ def singular_input_for(attribute,type,options={})
74
+ options.keep_if {|key, value| key != :for}
75
+ opts = html_options(attribute,type,options)
76
+ case type
77
+ when :string
78
+ case attribute
79
+ when /email/ ; email_field(attribute, opts);
80
+ when /password/ ; password_field(attribute, opts);
81
+ when /tel/,/phone/ ; tel_field(attribute, opts);
82
+ when /url/,/website/ ; url_field(attribute, opts);
83
+ when /search/ ; search_field(attribute, opts);
84
+ else ; text_field(attribute, opts);
85
+ end
86
+ when :text
87
+ text_area(attribute, opts)
88
+ when :boolean
89
+ check_box(attribute, opts)
90
+ when :date
91
+ date_field(attribute, opts)
92
+ when :file
93
+ file_field(attribute, opts)
94
+ when :number
95
+ number_field(attribute, opts)
96
+ when :radios
97
+ default_radios(attribute,type,options)
98
+ end
99
+ end
100
+
101
+ def required?(attribute)
102
+ k = klazz.is_a?(String) ? klazz.constantize : klazz
103
+ k.form_attribute_is_required?(attribute)
104
+ end
105
+
106
+ def collection_input(attribute,type,item, options={})
107
+ unchecked_value = options.delete(:uncheck_value) || '0'
108
+ options.reverse_merge!(:id => field_id(attribute), :value => '1')
109
+ options.reverse_merge!(:checked => true) if values_matches_field?(attribute, options[:value])
110
+ klass = css_class(attribute,type,options[:disabled])
111
+ name = type == :checks ? field_name(attribute) + '[]' : field_name(attribute)
112
+ if item.is_a?(Array)
113
+ text, value = item[0], item[1]
114
+ else
115
+ text = item; value = item;
116
+ end
117
+ id = field_id(attribute) + "_" + domize(text)
118
+ opts = html_options(attribute,type,options.merge(:id => id, :class => klass, :value => value))
119
+ if type == :checks
120
+ html = @template.hidden_field_tag(options[:name] || name, :value => unchecked_value, :id => nil)
121
+ input_item = @template.check_box_tag(name, opts)
122
+ else
123
+ html = ""
124
+ input_item = @template.radio_button_tag(name, opts)
125
+ end
126
+ html << "<label for='#{id}' class='#{klass}'>#{input_item}#{text}</label>"
127
+ end
128
+
129
+ def domize(text)
130
+ text.downcase.gsub(' ','_').gsub(/[^[:alnum:]]/, '')
131
+ end
132
+
133
+ def setup_label(attribute, type, options={})
134
+ marker = @@settings.label_required_marker
135
+ text = ""
136
+ text << marker if required?(attribute) && @@settings.label_required_marker_position == :prepend
137
+ text << field_human_name(attribute)
138
+ text << marker if required?(attribute) && @@settings.label_required_marker_position == :append
139
+ options.reverse_merge!(:caption => text, :class => css_class(attribute,type,options[:disabled]))
140
+ @template.label_tag(field_id(attribute), options)
141
+ end
142
+
143
+ def css_class(attribute,type,disabled=false)
144
+ klass = type.to_s
145
+ klass << ' required' if required?(attribute)
146
+ klass << ' required' if disabled
147
+ klass
148
+ end
149
+
150
+ def html_options(attribute,type,options)
151
+ options.keep_if {|key, value| key != :as}
152
+ html_class = options.merge(:class => css_class(attribute,type,options[:disabled]))
153
+ input_html = options[:input_html]
154
+ if input_html
155
+ html_class.merge(input_html) {|key, first, second| first + " " + second }.delete(:input_html)
156
+ else
157
+ html_class
158
+ end
159
+ end
160
+
161
+ def labelize(options)
162
+ opts = {}.merge(options)
163
+ opts.keep_if {|key, value| [:class,:id,:caption].include?(key)}
164
+ end
165
+
166
+ def hint(text)
167
+ @template.content_tag(:span, :class=>'hint') { text }
168
+ end
169
+
170
+ def default_radios(attribute,type,options)
171
+ [['yes',1],['no',0]].map do |item|
172
+ collection_input(attribute,:radios,item,options.keep_if {|key, value| key != :options})
173
+ end.join("")
174
+ end
175
+
176
+ def klazz
177
+ @object.class
178
+ end
179
+
180
+ end # StandardFormBuilder
181
+ end # FormBuilder
182
+ end # Helpers
183
+ end # Padrino
@@ -0,0 +1,63 @@
1
+ module Padrino
2
+ module Helpers
3
+ module FormHelpers
4
+
5
+ %w(date date email number search tel url).each do |type|
6
+ class_eval <<-EOF
7
+ ##
8
+ # Constructs a #{type} field input from the given options
9
+ #
10
+ # ==== Examples
11
+ #
12
+ # #{type}_field_tag :username, :class => 'long'
13
+ #
14
+ def #{type}_field_tag(name, options={})
15
+ options.reverse_merge!(:name => name)
16
+ input_tag(:#{type}, options)
17
+ end
18
+ EOF
19
+ end
20
+
21
+ def select_tag(name, options={})
22
+ options.reverse_merge!(:name => name)
23
+ collection, fields = options.delete(:collection), options.delete(:fields)
24
+ options[:options] = options_from_collection(collection, fields) if collection
25
+ prompt = options.delete(:include_blank)
26
+ select_options_html = if options[:options]
27
+ options_for_select(options.delete(:options), options.delete(:selected))
28
+ elsif options[:grouped_options]
29
+ grouped_options_for_select(options.delete(:grouped_options), options.delete(:selected), prompt)
30
+ end.unshift(blank_option(prompt))
31
+ options.merge!(:name => "#{options[:name]}[]") if options[:multiple]
32
+ content_tag(:select, select_options_html, options)
33
+ end
34
+
35
+ def grouped_options_for_select(collection,selected=nil,prompt=false)
36
+ if collection.is_a?(Hash)
37
+ collection.map do |key, value|
38
+ content_tag :optgroup, :label => key do
39
+ options_for_select(value, selected)
40
+ end
41
+ end
42
+ elsif collection.is_a?(Array)
43
+ collection.map do |optgroup|
44
+ content_tag :optgroup, :label => optgroup.first do
45
+ options_for_select(optgroup.last, selected)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def blank_option(prompt)
52
+ if prompt
53
+ case prompt.class.to_s
54
+ when 'String' ; content_tag(:option, prompt, :value => '') ;
55
+ when 'Array' ; content_tag(:option, prompt.first, :value => prompt.last) ;
56
+ else ; content_tag(:option, '', :value => '') ;
57
+ end
58
+ end
59
+ end
60
+
61
+ end # FormHelpers
62
+ end # Helpers
63
+ end # Padrino
@@ -0,0 +1,54 @@
1
+ require 'dm-core'
2
+ require 'dm-validations'
3
+
4
+ module PadrinoFields
5
+ module DataMapperWrapper
6
+ module ClassMethods
7
+
8
+ attr_reader :reflection
9
+
10
+ def form_attribute_validators; validators.contexts[:default]; end
11
+
12
+ def form_validators_on(attribute)
13
+ validators.contexts[:default].find_all {|v| v.field_name == attribute}
14
+ end
15
+
16
+ def form_attribute_is_required?(attribute)
17
+ form_validators_on(attribute).find_all {|v| v.class == DataMapper::Validations::PresenceValidator }.any?
18
+ end
19
+
20
+ def form_reflection_validators(reflection=nil)
21
+ new.send(reflection).validators.contexts[:default]
22
+ end
23
+
24
+ def form_has_required_attributes?(reflection=nil)
25
+ validators = form_attribute_validators
26
+ validators += reflection_validators(reflection) if reflection
27
+ validators.find_all {|v| v.class == DataMapper::Validations::PresenceValidator }.any?
28
+ end
29
+
30
+ def form_column_type_for(attribute)
31
+ klass = properties.find_all {|p| p.name == attribute}.first.class
32
+ if klass == DataMapper::Property::String
33
+ :string
34
+ elsif klass == DataMapper::Property::Text
35
+ :text
36
+ elsif [DataMapper::Property::Integer,DataMapper::Property::Decimal,DataMapper::Property::Float].include?(klass)
37
+ :number
38
+ elsif klass == DataMapper::Property::Boolean
39
+ :boolean
40
+ elsif [DataMapper::Property::Date,DataMapper::Property::DateTime].include?(klass)
41
+ :date
42
+ elsif klass == DataMapper::Property::Serial
43
+ :serial
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ end # ClassMethods
50
+ end # Datamapper
51
+ end # PadrinoFields
52
+
53
+ DataMapper::Model.append_extensions(PadrinoFields::DataMapperWrapper::ClassMethods)
54
+ #DataMapper::Model.append_inclusions(PadrinoFields::DataMapperWrapper::InstanceMethods)
@@ -0,0 +1,18 @@
1
+ module PadrinoFields
2
+ module Settings
3
+
4
+ mattr_accessor :container
5
+ @@container = :p
6
+
7
+ mattr_accessor :label_required_marker
8
+ @@label_required_marker = "<abbr>*</abbr>"
9
+
10
+ mattr_accessor :label_required_marker_position
11
+ @@label_required_marker_position = :prepend
12
+
13
+ def self.configure
14
+ yield self
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ begin
2
+ require File.expand_path('../.bundle/environment', __FILE__)
3
+ rescue LoadError
4
+ if defined?(Gem)
5
+ Gem.cache
6
+ gem 'bundler'
7
+ else
8
+ require 'rubygems'
9
+ end
10
+ require 'bundler'
11
+ Bundler.setup
12
+ end
@@ -0,0 +1,141 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{padrino-fields}
8
+ s.version = "0.3.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Steven Garcia"]
12
+ s.date = %q{2011-03-17}
13
+ s.description = %q{Smart fields for your forms, similar to Formtastic or SimpleForm}
14
+ s.email = %q{stevendgarcia@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.markdown"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "Gemfile",
22
+ "Gemfile.lock",
23
+ "LICENSE.txt",
24
+ "README.markdown",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "lib/padrino-fields.rb",
28
+ "lib/padrino-fields/form_builder.rb",
29
+ "lib/padrino-fields/form_helpers.rb",
30
+ "lib/padrino-fields/orms/datamapper.rb",
31
+ "lib/padrino-fields/settings.rb",
32
+ "load_paths.rb",
33
+ "padrino-fields.gemspec",
34
+ "test/.DS_Store",
35
+ "test/fixtures/datamapper/app.rb",
36
+ "test/fixtures/datamapper/views/capture_concat.erb",
37
+ "test/fixtures/datamapper/views/capture_concat.haml",
38
+ "test/fixtures/datamapper/views/content_for.erb",
39
+ "test/fixtures/datamapper/views/content_for.haml",
40
+ "test/fixtures/datamapper/views/content_tag.erb",
41
+ "test/fixtures/datamapper/views/content_tag.haml",
42
+ "test/fixtures/datamapper/views/fields_for.erb",
43
+ "test/fixtures/datamapper/views/fields_for.haml",
44
+ "test/fixtures/datamapper/views/form_for.erb",
45
+ "test/fixtures/datamapper/views/form_for.haml",
46
+ "test/fixtures/datamapper/views/form_tag.erb",
47
+ "test/fixtures/datamapper/views/form_tag.haml",
48
+ "test/fixtures/datamapper/views/link_to.erb",
49
+ "test/fixtures/datamapper/views/link_to.haml",
50
+ "test/fixtures/datamapper/views/mail_to.erb",
51
+ "test/fixtures/datamapper/views/mail_to.haml",
52
+ "test/fixtures/datamapper/views/meta_tag.erb",
53
+ "test/fixtures/datamapper/views/meta_tag.haml",
54
+ "test/helper.rb",
55
+ "test/test_datamapper.rb",
56
+ "test/test_form_builder.rb",
57
+ "test/test_form_helpers.rb",
58
+ "test/test_settings.rb"
59
+ ]
60
+ s.homepage = %q{http://github.com/activestylus/padrino-fields}
61
+ s.licenses = ["MIT"]
62
+ s.require_paths = ["lib"]
63
+ s.rubygems_version = %q{1.3.7}
64
+ s.summary = %q{Advanced form helpers for Padrino framework}
65
+ s.test_files = [
66
+ "test/fixtures/datamapper/app.rb",
67
+ "test/helper.rb",
68
+ "test/test_datamapper.rb",
69
+ "test/test_form_builder.rb",
70
+ "test/test_form_helpers.rb",
71
+ "test/test_settings.rb"
72
+ ]
73
+
74
+ if s.respond_to? :specification_version then
75
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
76
+ s.specification_version = 3
77
+
78
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
79
+ s.add_runtime_dependency(%q<padrino>, [">= 0"])
80
+ s.add_runtime_dependency(%q<padrino-core>, [">= 0"])
81
+ s.add_runtime_dependency(%q<padrino-helpers>, [">= 0"])
82
+ s.add_development_dependency(%q<rcov>, [">= 0"])
83
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
84
+ s.add_development_dependency(%q<rake>, [">= 0.8.7"])
85
+ s.add_development_dependency(%q<mocha>, [">= 0.9.8"])
86
+ s.add_development_dependency(%q<rack-test>, [">= 0.5.0"])
87
+ s.add_development_dependency(%q<fakeweb>, [">= 1.2.8"])
88
+ s.add_development_dependency(%q<webrat>, [">= 0.5.1"])
89
+ s.add_development_dependency(%q<haml>, [">= 2.2.22"])
90
+ s.add_development_dependency(%q<phocus>, [">= 0"])
91
+ s.add_development_dependency(%q<shoulda>, [">= 2.10.3"])
92
+ s.add_development_dependency(%q<uuid>, [">= 2.3.1"])
93
+ s.add_development_dependency(%q<bcrypt-ruby>, [">= 0"])
94
+ s.add_development_dependency(%q<ruby-prof>, [">= 0.9.1"])
95
+ s.add_development_dependency(%q<system_timer>, [">= 1.0"])
96
+ s.add_development_dependency(%q<memcached>, [">= 0.20.1"])
97
+ s.add_development_dependency(%q<dalli>, [">= 1.0.2"])
98
+ else
99
+ s.add_dependency(%q<padrino>, [">= 0"])
100
+ s.add_dependency(%q<padrino-core>, [">= 0"])
101
+ s.add_dependency(%q<padrino-helpers>, [">= 0"])
102
+ s.add_dependency(%q<rcov>, [">= 0"])
103
+ s.add_dependency(%q<jeweler>, [">= 0"])
104
+ s.add_dependency(%q<rake>, [">= 0.8.7"])
105
+ s.add_dependency(%q<mocha>, [">= 0.9.8"])
106
+ s.add_dependency(%q<rack-test>, [">= 0.5.0"])
107
+ s.add_dependency(%q<fakeweb>, [">= 1.2.8"])
108
+ s.add_dependency(%q<webrat>, [">= 0.5.1"])
109
+ s.add_dependency(%q<haml>, [">= 2.2.22"])
110
+ s.add_dependency(%q<phocus>, [">= 0"])
111
+ s.add_dependency(%q<shoulda>, [">= 2.10.3"])
112
+ s.add_dependency(%q<uuid>, [">= 2.3.1"])
113
+ s.add_dependency(%q<bcrypt-ruby>, [">= 0"])
114
+ s.add_dependency(%q<ruby-prof>, [">= 0.9.1"])
115
+ s.add_dependency(%q<system_timer>, [">= 1.0"])
116
+ s.add_dependency(%q<memcached>, [">= 0.20.1"])
117
+ s.add_dependency(%q<dalli>, [">= 1.0.2"])
118
+ end
119
+ else
120
+ s.add_dependency(%q<padrino>, [">= 0"])
121
+ s.add_dependency(%q<padrino-core>, [">= 0"])
122
+ s.add_dependency(%q<padrino-helpers>, [">= 0"])
123
+ s.add_dependency(%q<rcov>, [">= 0"])
124
+ s.add_dependency(%q<jeweler>, [">= 0"])
125
+ s.add_dependency(%q<rake>, [">= 0.8.7"])
126
+ s.add_dependency(%q<mocha>, [">= 0.9.8"])
127
+ s.add_dependency(%q<rack-test>, [">= 0.5.0"])
128
+ s.add_dependency(%q<fakeweb>, [">= 1.2.8"])
129
+ s.add_dependency(%q<webrat>, [">= 0.5.1"])
130
+ s.add_dependency(%q<haml>, [">= 2.2.22"])
131
+ s.add_dependency(%q<phocus>, [">= 0"])
132
+ s.add_dependency(%q<shoulda>, [">= 2.10.3"])
133
+ s.add_dependency(%q<uuid>, [">= 2.3.1"])
134
+ s.add_dependency(%q<bcrypt-ruby>, [">= 0"])
135
+ s.add_dependency(%q<ruby-prof>, [">= 0.9.1"])
136
+ s.add_dependency(%q<system_timer>, [">= 1.0"])
137
+ s.add_dependency(%q<memcached>, [">= 0.20.1"])
138
+ s.add_dependency(%q<dalli>, [">= 1.0.2"])
139
+ end
140
+ end
141
+