bureaucrat 0.0.3 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +120 -105
- data/Rakefile +9 -0
- data/bureaucrat.gemspec +32 -0
- data/lib/bureaucrat.rb +26 -8
- data/lib/bureaucrat/fields.rb +572 -336
- data/lib/bureaucrat/forms.rb +296 -257
- data/lib/bureaucrat/formsets.rb +235 -194
- data/lib/bureaucrat/quickfields.rb +90 -62
- data/lib/bureaucrat/temporary_uploaded_file.rb +14 -0
- data/lib/bureaucrat/utils.rb +79 -62
- data/lib/bureaucrat/validators.rb +163 -0
- data/lib/bureaucrat/widgets.rb +459 -303
- data/test/fields_test.rb +519 -380
- data/test/forms_test.rb +78 -58
- data/test/formsets_test.rb +48 -22
- data/test/test_helper.rb +20 -12
- data/test/widgets_test.rb +224 -204
- metadata +27 -36
- data/lib/bureaucrat/validation.rb +0 -130
- data/lib/bureaucrat/validation_old.rb +0 -148
- data/lib/bureaucrat/wizard.rb +0 -220
data/README.md
CHANGED
@@ -1,124 +1,139 @@
|
|
1
1
|
Bureaucrat
|
2
2
|
==========
|
3
3
|
|
4
|
-
Form handling for Ruby inspired by Django forms.
|
4
|
+
Form handling for Ruby inspired by [Django forms](https://docs.djangoproject.com/en/dev/#forms).
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
Description
|
7
|
+
-----------
|
8
8
|
|
9
|
-
|
9
|
+
Bureaucrat is a library for handling the processing, validation and rendering of HTML forms.
|
10
|
+
|
11
|
+
Structure of a Form
|
12
|
+
-------------------
|
13
|
+
|
14
|
+
Form ----> valid?, errors/cleaned_data
|
10
15
|
______|________
|
11
16
|
/ | \
|
12
17
|
Field Field Field ----> clean
|
13
18
|
| | |
|
14
19
|
Widget Widget Widget ----> render
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
**Form**:
|
22
|
+
Collection of named Fields, handles global validation and the last pass of
|
23
|
+
data conversion.
|
24
|
+
After validation, a valid Form responds to `cleaned_data` by returning a
|
25
|
+
hash of validated values and an invalid Form responds to `errors` by
|
26
|
+
returning a hash of field_name => error_messages.
|
27
|
+
|
28
|
+
**Field**:
|
29
|
+
Handles the validation and data conversion of each field belonging to the Form. Each Field is associated to a name on the parent Form.
|
30
|
+
|
31
|
+
**Widget**:
|
32
|
+
Handles the rendering of a Form field. Each Field has two widgets associated, one for normal rendering, and another for hidden inputs rendering. Every type of Field has default Widgets defined, but they can be overriden on a per-Form basis.
|
23
33
|
|
24
34
|
Usage examples
|
25
35
|
--------------
|
26
36
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
37
|
+
```ruby
|
38
|
+
require 'bureaucrat'
|
39
|
+
require 'bureaucrat/quickfields'
|
40
|
+
|
41
|
+
class MyForm < Bureaucrat::Forms::Form
|
42
|
+
extend Bureaucrat::Quickfields
|
43
|
+
|
44
|
+
string :nickname, max_length: 50
|
45
|
+
string :realname, required: false
|
46
|
+
email :email
|
47
|
+
integer :age, min_value: 0
|
48
|
+
boolean :newsletter, required: false
|
49
|
+
|
50
|
+
# Note: Bureaucrat doesn't define save
|
51
|
+
def save
|
52
|
+
user = User.create!(cleaned_data)
|
53
|
+
Mailer.deliver_confirmation_mail(user)
|
54
|
+
user
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# A Form initialized without parameters is an unbound Form.
|
59
|
+
unbound_form = MyForm.new
|
60
|
+
unbound_form.valid? # => false
|
61
|
+
unbound_form.errors # => {}
|
62
|
+
unbound_form.cleaned_data # => nil
|
63
|
+
puts unbound_form.as_p
|
64
|
+
# Prints:
|
65
|
+
# <p><label for="id_nickname">Nickname:</label> <input type="text" name="nickname" id="id_nickname" maxlength="50" /></p>
|
66
|
+
# <p><label for="id_realname">Realname:</label> <input type="text" name="realname" id="id_realname" /></p>
|
67
|
+
# <p><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></p>
|
68
|
+
# <p><label for="id_age">Age:</label> <input type="text" name="age" id="id_age" /></p>
|
69
|
+
# <p><label for="id_newsletter">Newsletter:</label> <input type="checkbox" name="newsletter" id="id_newsletter" /></p>
|
70
|
+
|
71
|
+
invalid_bound_form = MyForm.new(nickname: 'bureaucrat', email: 'badformat', age: '30')
|
72
|
+
invalid_bound_form.valid? # => false
|
73
|
+
invalid_bound_form.errors # {email: ["Enter a valid e-mail address."]}
|
74
|
+
invalid_bound_form.cleaned_data # => nil
|
75
|
+
puts invalid_bound_form.as_table
|
76
|
+
# Prints:
|
77
|
+
# <tr><th><label for="id_nickname">Nickname:</label></th><td><input type="text" value="bureaucrat" name="nickname" id="id_nickname" maxlength="50" /></td></tr>
|
78
|
+
# <tr><th><label for="id_realname">Realname:</label></th><td><ul class="errorlist"><li>This field is required</li></ul><input type="text" name="realname" id="id_realname" /></td></tr>
|
79
|
+
# <tr><th><label for="id_email">Email:</label></th><td><ul class="errorlist"><li>Enter a valid e-mail address.</li></ul><input type="text" value="badformat" name="email" id="id_email" /></td></tr>
|
80
|
+
# <tr><th><label for="id_age">Age:</label></th><td><input type="text" value="30" name="age" id="id_age" /></td></tr>
|
81
|
+
# <tr><th><label for="id_newsletter">Newsletter:</label></th><td><input type="checkbox" name="newsletter" id="id_newsletter" /></td></tr>
|
82
|
+
|
83
|
+
valid_bound_form = MyForm.new(nickname: 'bureaucrat', email: 'valid@email.com', age: '30')
|
84
|
+
valid_bound_form.valid? # => true
|
85
|
+
valid_bound_form.errors # {}
|
86
|
+
valid_bound_form.cleaned_data # => {age: 30, newsletter: false, nickname: "bureaucrat", realname: "", :email = >"valid@email.com"}
|
87
|
+
puts valid_bound_form.as_ul
|
88
|
+
# Prints:
|
89
|
+
# <li><label for="id_nickname">Nickname:</label> <input type="text" value="bureaucrat" name="nickname" id="id_nickname" maxlength="50" /></li>
|
90
|
+
# <li><ul class="errorlist"><li>This field is required</li></ul><label for="id_realname">Realname:</label> <input type="text" name="realname" id="id_realname" /></li>
|
91
|
+
# <li><label for="id_email">Email:</label> <input type="text" value="valid@email.com" name="email" id="id_email" /></li>
|
92
|
+
# <li><label for="id_age">Age:</label> <input type="text" value="30" name="age" id="id_age" /></li>
|
93
|
+
# <li><label for="id_newsletter">Newsletter:</label> <input type="checkbox" name="newsletter" id="id_newsletter" /></li>
|
94
|
+
|
95
|
+
valid_bound_form.save # A new User is created and a confirmation mail is delivered
|
96
|
+
```
|
84
97
|
|
85
98
|
Examples of different ways of defining forms
|
86
99
|
--------------
|
87
100
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
101
|
+
```ruby
|
102
|
+
require 'bureaucrat'
|
103
|
+
require 'bureaucrat/quickfields'
|
104
|
+
|
105
|
+
class MyForm < Bureaucrat::Forms::Form
|
106
|
+
include Bureaucrat::Fields
|
107
|
+
|
108
|
+
field :nickname, CharField.new(max_length: 50)
|
109
|
+
field :realname, CharField.new(required: false)
|
110
|
+
field :email, EmailField.new
|
111
|
+
field :age, IntegerField.new(min_value: 0)
|
112
|
+
field :newsletter, BooleanField.new(required: false)
|
113
|
+
end
|
114
|
+
|
115
|
+
class MyFormQuick < Bureaucrat::Forms::Form
|
116
|
+
extend Bureaucrat::Quickfields
|
117
|
+
|
118
|
+
string :nickname, max_length: 50
|
119
|
+
string :realname, required: false
|
120
|
+
email :email
|
121
|
+
integer :age, min_value: 0
|
122
|
+
boolean :newsletter, required: false
|
123
|
+
end
|
124
|
+
|
125
|
+
def inline_form
|
126
|
+
f = Class.new(Bureaucrat::Forms::Form)
|
127
|
+
f.extend(Bureaucrat::Quickfields)
|
128
|
+
yield f
|
129
|
+
f
|
130
|
+
end
|
131
|
+
|
132
|
+
form_maker = inline_form do |f|
|
133
|
+
f.string :nickname, max_length: 50
|
134
|
+
f.string :realname, required: false
|
135
|
+
f.email :email
|
136
|
+
f.integer :age, min_value: 0
|
137
|
+
f.boolean :newsletter, required: false
|
138
|
+
end
|
139
|
+
```
|
data/Rakefile
ADDED
data/bureaucrat.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'bureaucrat'
|
3
|
+
s.version = '0.10.0'
|
4
|
+
s.summary = "Form handling for Ruby inspired by Django forms."
|
5
|
+
s.description = "Bureaucrat is a form handling library for Ruby."
|
6
|
+
s.author = "Bruno Deferrari"
|
7
|
+
s.email = "utizoc@gmail.com"
|
8
|
+
s.homepage = "http://github.com/tizoc/bureaucrat"
|
9
|
+
|
10
|
+
s.files = [
|
11
|
+
"lib/bureaucrat/fields.rb",
|
12
|
+
"lib/bureaucrat/forms.rb",
|
13
|
+
"lib/bureaucrat/formsets.rb",
|
14
|
+
"lib/bureaucrat/quickfields.rb",
|
15
|
+
"lib/bureaucrat/temporary_uploaded_file.rb",
|
16
|
+
"lib/bureaucrat/utils.rb",
|
17
|
+
"lib/bureaucrat/validators.rb",
|
18
|
+
"lib/bureaucrat/widgets.rb",
|
19
|
+
"lib/bureaucrat.rb",
|
20
|
+
"README.md",
|
21
|
+
"LICENSE",
|
22
|
+
"test/fields_test.rb",
|
23
|
+
"test/forms_test.rb",
|
24
|
+
"test/formsets_test.rb",
|
25
|
+
"test/test_helper.rb",
|
26
|
+
"test/widgets_test.rb",
|
27
|
+
"Rakefile",
|
28
|
+
"bureaucrat.gemspec"
|
29
|
+
]
|
30
|
+
|
31
|
+
s.require_paths = ['lib']
|
32
|
+
end
|
data/lib/bureaucrat.rb
CHANGED
@@ -1,10 +1,28 @@
|
|
1
|
-
libdir = File.dirname(__FILE__)
|
2
|
-
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
3
|
-
|
4
1
|
module Bureaucrat
|
5
|
-
VERSION = '0.0
|
6
|
-
|
2
|
+
VERSION = '0.10.0'
|
3
|
+
|
4
|
+
class ValidationError < Exception
|
5
|
+
attr_reader :code, :params, :messages
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def initialize(message, code = nil, params = nil)
|
8
|
+
if message.is_a? Array
|
9
|
+
@messages = message
|
10
|
+
else
|
11
|
+
@code = code
|
12
|
+
@params = params
|
13
|
+
@messages = [message]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"ValidationError(#{@messages.inspect})"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
require_relative 'bureaucrat/utils'
|
23
|
+
require_relative 'bureaucrat/validators'
|
24
|
+
require_relative 'bureaucrat/widgets'
|
25
|
+
require_relative 'bureaucrat/fields'
|
26
|
+
require_relative 'bureaucrat/forms'
|
27
|
+
require_relative 'bureaucrat/temporary_uploaded_file'
|
28
|
+
end
|
data/lib/bureaucrat/fields.rb
CHANGED
@@ -1,461 +1,697 @@
|
|
1
|
-
require '
|
2
|
-
require 'bureaucrat/validation'
|
3
|
-
require 'bureaucrat/widgets'
|
1
|
+
require 'set'
|
4
2
|
|
5
3
|
module Bureaucrat
|
6
|
-
module Fields
|
4
|
+
module Fields
|
7
5
|
|
8
|
-
|
9
|
-
|
6
|
+
class ErrorList < Array
|
7
|
+
include Utils
|
10
8
|
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
def to_s
|
10
|
+
as_ul
|
11
|
+
end
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
def as_ul
|
14
|
+
if empty?
|
15
|
+
''
|
16
|
+
else
|
17
|
+
ul = '<ul class="errorlist">%s</ul>'
|
18
|
+
li = '<li>%s</li>'
|
20
19
|
|
21
|
-
|
22
|
-
|
20
|
+
result = ul % map{|e| li % conditional_escape(e)}.join("\n")
|
21
|
+
mark_safe(result)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_text
|
26
|
+
empty? ? '' : map{|e| '* %s' % e}.join("\n")
|
27
|
+
end
|
23
28
|
end
|
24
|
-
end
|
25
29
|
|
26
|
-
|
27
|
-
|
30
|
+
class ErrorHash < Hash
|
31
|
+
include Utils
|
28
32
|
|
29
|
-
|
30
|
-
|
31
|
-
|
33
|
+
def to_s
|
34
|
+
as_ul
|
35
|
+
end
|
32
36
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
37
|
+
def as_ul
|
38
|
+
ul = '<ul class="errorlist">%s</ul>'
|
39
|
+
li = '<li>%s%s</li>'
|
40
|
+
empty? ? '' : mark_safe(ul % map {|k, v| li % [k, v]}.join)
|
41
|
+
end
|
38
42
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
def as_text
|
44
|
+
map do |k, v|
|
45
|
+
"* %s\n%s" % [k, v.map{|i| ' * %s'}.join("\n")]
|
46
|
+
end.join("\n")
|
47
|
+
end
|
43
48
|
end
|
44
|
-
end
|
45
49
|
|
46
|
-
|
47
|
-
|
50
|
+
class Field
|
51
|
+
attr_accessor :required, :label, :initial, :error_messages, :widget, :hidden_widget, :show_hidden_initial, :help_text, :validators
|
48
52
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
+
def initialize(options={})
|
54
|
+
@required = options.fetch(:required, true)
|
55
|
+
@show_hidden_initial = options.fetch(:show_hidden_initial, false)
|
56
|
+
@label = options[:label]
|
57
|
+
@initial = options[:initial]
|
58
|
+
@help_text = options.fetch(:help_text, '')
|
59
|
+
@widget = options.fetch(:widget, default_widget)
|
53
60
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
61
|
+
@widget = @widget.new if @widget.is_a?(Class)
|
62
|
+
@widget.attrs.update(widget_attrs(@widget))
|
63
|
+
@widget.is_required = @required
|
58
64
|
|
59
|
-
|
60
|
-
|
61
|
-
include Validation::Converters
|
65
|
+
@hidden_widget = options.fetch(:hidden_widget, default_hidden_widget)
|
66
|
+
@hidden_widget = @hidden_widget.new if @hidden_widget.is_a?(Class)
|
62
67
|
|
63
|
-
|
64
|
-
|
68
|
+
@error_messages = default_error_messages.
|
69
|
+
merge(options.fetch(:error_messages, {}))
|
65
70
|
|
66
|
-
|
67
|
-
@default_error_messages ||= {}
|
68
|
-
@default_error_messages[key] = template
|
71
|
+
@validators = default_validators + options.fetch(:validators, [])
|
69
72
|
end
|
70
73
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
+
# Default error messages for this kind of field. Override on subclasses to add or replace messages
|
75
|
+
def default_error_messages
|
76
|
+
{
|
77
|
+
required: 'This field is required',
|
78
|
+
invalid: 'Enter a valid value'
|
79
|
+
}
|
74
80
|
end
|
75
81
|
|
76
|
-
|
77
|
-
|
78
|
-
|
82
|
+
# Default validators for this kind of field.
|
83
|
+
def default_validators
|
84
|
+
[]
|
79
85
|
end
|
80
86
|
|
81
|
-
#
|
82
|
-
def
|
83
|
-
|
84
|
-
c.hidden_widget hidden_widget
|
85
|
-
default_error_messages.each {|k, v| c.set_error k, v}
|
87
|
+
# Default widget for this kind of field. Override on subclasses to customize.
|
88
|
+
def default_widget
|
89
|
+
Widgets::TextInput
|
86
90
|
end
|
87
|
-
end
|
88
91
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
set_error :invalid, 'Enter a valid value'
|
92
|
+
# Default hidden widget for this kind of field. Override on subclasses to customize.
|
93
|
+
def default_hidden_widget
|
94
|
+
Widgets::HiddenInput
|
95
|
+
end
|
94
96
|
|
95
|
-
|
97
|
+
def prepare_value(value)
|
98
|
+
value
|
99
|
+
end
|
96
100
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
@label = options[:label]
|
101
|
-
@initial = options[:initial]
|
102
|
-
@help_text = options.fetch(:help_text, '')
|
103
|
-
@widget = options.fetch(:widget, self.class.widget)
|
101
|
+
def to_object(value)
|
102
|
+
value
|
103
|
+
end
|
104
104
|
|
105
|
-
|
106
|
-
|
107
|
-
|
105
|
+
def validate(value)
|
106
|
+
if required && Validators.empty_value?(value)
|
107
|
+
raise ValidationError.new(error_messages[:required])
|
108
|
+
end
|
109
|
+
end
|
108
110
|
|
109
|
-
|
110
|
-
|
111
|
-
|
111
|
+
def run_validators(value)
|
112
|
+
if Validators.empty_value?(value)
|
113
|
+
return
|
114
|
+
end
|
112
115
|
|
113
|
-
|
114
|
-
yield
|
115
|
-
rescue Validation::ValidationError => error
|
116
|
-
tpl = error_messages.fetch(error.error_code, error.error_code.to_s)
|
117
|
-
msg = Utils.format_string(tpl, error.parameters)
|
118
|
-
raise FieldValidationError.new(msg)
|
119
|
-
end
|
116
|
+
errors = []
|
120
117
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
118
|
+
validators.each do |v|
|
119
|
+
begin
|
120
|
+
v.call(value)
|
121
|
+
rescue ValidationError => e
|
122
|
+
if e.code && error_messages.has_key?(e.code)
|
123
|
+
message = error_messages[e.code]
|
124
|
+
|
125
|
+
if e.params
|
126
|
+
message = Utils.format_string(message, e.params)
|
127
|
+
end
|
128
|
+
|
129
|
+
errors << message
|
130
|
+
else
|
131
|
+
errors += e.messages
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
125
135
|
|
126
|
-
|
127
|
-
|
128
|
-
|
136
|
+
unless errors.empty?
|
137
|
+
raise ValidationError.new(errors)
|
138
|
+
end
|
139
|
+
end
|
129
140
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
141
|
+
def clean(value)
|
142
|
+
value = to_object(value)
|
143
|
+
validate(value)
|
144
|
+
run_validators(value)
|
145
|
+
value
|
146
|
+
end
|
136
147
|
|
137
|
-
|
148
|
+
# The data to be displayed when rendering for a bound form
|
149
|
+
def bound_data(data, initial)
|
150
|
+
data
|
151
|
+
end
|
138
152
|
|
139
|
-
|
140
|
-
|
141
|
-
|
153
|
+
# List of attributes to add on the widget. Override to add field specific attributes
|
154
|
+
def widget_attrs(widget)
|
155
|
+
{}
|
156
|
+
end
|
142
157
|
|
143
|
-
|
158
|
+
# Populates object.name if posible
|
159
|
+
def populate_object(object, name, value)
|
160
|
+
setter = :"#{name}="
|
144
161
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
162
|
+
if object.respond_to?(setter)
|
163
|
+
object.send(setter, value)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def initialize_copy(original)
|
168
|
+
super(original)
|
169
|
+
@initial = original.initial
|
170
|
+
begin
|
171
|
+
@initial = @initial.dup
|
172
|
+
rescue TypeError
|
173
|
+
# non-clonable
|
174
|
+
end
|
175
|
+
@label = original.label && original.label.dup
|
176
|
+
@widget = original.widget && original.widget.dup
|
177
|
+
@validators = original.validators.dup
|
178
|
+
@error_messages = original.error_messages.dup
|
179
|
+
end
|
150
180
|
|
151
|
-
def widget_attrs(widget)
|
152
|
-
return if @max_length.nil?
|
153
|
-
return {:maxlength => @max_length.to_s} if
|
154
|
-
widget.kind_of?(Widgets::TextInput) || widget.kind_of?(Widgets::PasswordInput)
|
155
181
|
end
|
156
182
|
|
157
|
-
|
158
|
-
|
159
|
-
|
183
|
+
class CharField < Field
|
184
|
+
attr_accessor :max_length, :min_length
|
185
|
+
|
186
|
+
def initialize(options = {})
|
187
|
+
@max_length = options.delete(:max_length)
|
188
|
+
@min_length = options.delete(:min_length)
|
189
|
+
super(options)
|
160
190
|
|
161
|
-
|
162
|
-
|
163
|
-
has_min_length(value, @min_length) if @min_length
|
191
|
+
if @min_length
|
192
|
+
validators << Validators::MinLengthValidator.new(@min_length)
|
164
193
|
end
|
165
194
|
|
166
|
-
|
167
|
-
|
168
|
-
|
195
|
+
if @max_length
|
196
|
+
validators << Validators::MaxLengthValidator.new(@max_length)
|
197
|
+
end
|
198
|
+
end
|
169
199
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
200
|
+
def to_object(value)
|
201
|
+
if Validators.empty_value?(value)
|
202
|
+
''
|
203
|
+
else
|
204
|
+
value
|
205
|
+
end
|
206
|
+
end
|
174
207
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
208
|
+
def widget_attrs(widget)
|
209
|
+
super(widget).tap do |attrs|
|
210
|
+
if @max_length && (widget.kind_of?(Widgets::TextInput) ||
|
211
|
+
widget.kind_of?(Widgets::PasswordInput))
|
212
|
+
attrs.merge(maxlength: @max_length.to_s)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
179
216
|
end
|
180
217
|
|
181
|
-
|
182
|
-
|
183
|
-
|
218
|
+
class IntegerField < Field
|
219
|
+
def initialize(options={})
|
220
|
+
@max_value = options.delete(:max_value)
|
221
|
+
@min_value = options.delete(:min_value)
|
222
|
+
super(options)
|
184
223
|
|
185
|
-
|
186
|
-
|
187
|
-
is_not_greater_than(value, @max_value) if @max_value
|
188
|
-
is_not_lesser_than(value, @min_value) if @min_value
|
224
|
+
if @min_value
|
225
|
+
validators << Validators::MinValueValidator.new(@min_value)
|
189
226
|
end
|
190
227
|
|
191
|
-
|
192
|
-
|
193
|
-
|
228
|
+
if @max_value
|
229
|
+
validators << Validators::MaxValueValidator.new(@max_value)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def default_error_messages
|
234
|
+
super.merge(invalid: 'Enter a whole number.',
|
235
|
+
max_value: 'Ensure this value is less than or equal to %(max)s.',
|
236
|
+
min_value: 'Ensure this value is greater than or equal to %(min)s.')
|
237
|
+
end
|
238
|
+
|
239
|
+
def to_object(value)
|
240
|
+
value = super(value)
|
194
241
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
set_error :min_value, 'Ensure this value is greater than or equal to %(min)s.'
|
242
|
+
if Validators.empty_value?(value)
|
243
|
+
return nil
|
244
|
+
end
|
199
245
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
246
|
+
begin
|
247
|
+
Integer(value.to_s)
|
248
|
+
rescue ArgumentError
|
249
|
+
raise ValidationError.new(error_messages[:invalid])
|
250
|
+
end
|
251
|
+
end
|
204
252
|
end
|
205
253
|
|
206
|
-
|
207
|
-
|
208
|
-
|
254
|
+
class FloatField < IntegerField
|
255
|
+
def default_error_messages
|
256
|
+
super.merge(invalid: 'Enter a number.')
|
257
|
+
end
|
209
258
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
is_not_lesser_than(value, @min_value) if @min_value
|
259
|
+
def to_object(value)
|
260
|
+
if Validators.empty_value?(value)
|
261
|
+
return nil
|
214
262
|
end
|
215
263
|
|
216
|
-
|
264
|
+
begin
|
265
|
+
Utils.make_float(value.to_s)
|
266
|
+
rescue ArgumentError
|
267
|
+
raise ValidationError.new(error_messages[:invalid])
|
268
|
+
end
|
269
|
+
end
|
217
270
|
end
|
218
|
-
end
|
219
271
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
@min_value = options.delete(:min_value)
|
231
|
-
@max_digits = options.delete(:max_digits)
|
232
|
-
@max_decimal_places = options.delete(:max_decimal_places)
|
233
|
-
@whole_digits = @max_digits - @decimal_places if @max_digits && @decimal_places
|
234
|
-
super(options)
|
235
|
-
end
|
272
|
+
class BigDecimalField < Field
|
273
|
+
def initialize(options={})
|
274
|
+
@max_value = options.delete(:max_value)
|
275
|
+
@min_value = options.delete(:min_value)
|
276
|
+
@max_digits = options.delete(:max_digits)
|
277
|
+
@max_decimal_places = options.delete(:max_decimal_places)
|
278
|
+
|
279
|
+
if @max_digits && @max_decimal_places
|
280
|
+
@max_whole_digits = @max_digits - @decimal_places
|
281
|
+
end
|
236
282
|
|
237
|
-
|
238
|
-
super(value)
|
239
|
-
return nil if !@required && empty_value?(value)
|
283
|
+
super(options)
|
240
284
|
|
241
|
-
|
242
|
-
|
243
|
-
is_not_greater_than(value, @max_value) if @max_value
|
244
|
-
is_not_lesser_than(value, @min_value) if @min_value
|
245
|
-
has_max_digits(value, @max_digits) if @max_digits
|
246
|
-
has_max_decimal_places(value, @max_decimal_places) if @max_decimal_places
|
247
|
-
has_max_whole_digits(value, @max_whole_digits) if @max_whole_digits
|
285
|
+
if @min_value
|
286
|
+
validators << Validators::MinValueValidator.new(@min_value)
|
248
287
|
end
|
249
288
|
|
250
|
-
|
251
|
-
|
252
|
-
|
289
|
+
if @max_value
|
290
|
+
validators << Validators::MaxValueValidator.new(@max_value)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def default_error_messages
|
295
|
+
super.merge(invalid: 'Enter a number.',
|
296
|
+
max_value: 'Ensure this value is less than or equal to %(max)s.',
|
297
|
+
min_value: 'Ensure this value is greater than or equal to %(min)s.',
|
298
|
+
max_digits: 'Ensure that there are no more than %(max)s digits in total.',
|
299
|
+
max_decimal_places: 'Ensure that there are no more than %(max)s decimal places.',
|
300
|
+
max_whole_digits: 'Ensure that there are no more than %(max)s digits before the decimal point.')
|
301
|
+
end
|
253
302
|
|
254
|
-
|
255
|
-
|
256
|
-
|
303
|
+
def to_object(value)
|
304
|
+
if Validators.empty_value?(value)
|
305
|
+
return nil
|
306
|
+
end
|
257
307
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
308
|
+
begin
|
309
|
+
Utils.make_float(value)
|
310
|
+
BigDecimal.new(value)
|
311
|
+
rescue ArgumentError
|
312
|
+
raise ValidationError.new(error_messages[:invalid])
|
313
|
+
end
|
264
314
|
end
|
265
|
-
super(options)
|
266
|
-
@regex = regex
|
267
|
-
end
|
268
315
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
316
|
+
def validate(value)
|
317
|
+
super(value)
|
318
|
+
|
319
|
+
if Validators.empty_value?(value)
|
320
|
+
return nil
|
321
|
+
end
|
322
|
+
|
323
|
+
if value.nan? || value.infinite?
|
324
|
+
raise ValidationError.new(error_messages[:invalid])
|
325
|
+
end
|
326
|
+
|
327
|
+
sign, alldigits, _, whole_digits = value.split
|
328
|
+
|
329
|
+
if @max_digits && alldigits.length > @max_digits
|
330
|
+
msg = Utils.format_string(error_messages[:max_digits],
|
331
|
+
max: @max_digits)
|
332
|
+
raise ValidationError.new(msg)
|
333
|
+
end
|
334
|
+
|
335
|
+
decimals = alldigits.length - whole_digits
|
336
|
+
|
337
|
+
if @max_decimal_places && decimals > @max_decimal_places
|
338
|
+
msg = Utils.format_string(error_messages[:max_decimal_places],
|
339
|
+
max: @max_decimal_places)
|
340
|
+
raise ValidationError.new(msg)
|
341
|
+
end
|
342
|
+
|
343
|
+
if @max_whole_digits && whole_digits > @max_whole_digits
|
344
|
+
msg = Utils.format_string(error_messages[:max_whole_digits],
|
345
|
+
max: @max_whole_digits)
|
346
|
+
raise ValidationError.new(msg)
|
347
|
+
end
|
348
|
+
|
349
|
+
value
|
350
|
+
end
|
274
351
|
end
|
275
|
-
end
|
276
352
|
|
277
|
-
|
278
|
-
#
|
279
|
-
#
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
353
|
+
# DateField
|
354
|
+
# TimeField
|
355
|
+
# DateTimeField
|
356
|
+
|
357
|
+
class RegexField < CharField
|
358
|
+
def initialize(regex, options={})
|
359
|
+
error_message = options.delete(:error_message)
|
360
|
+
|
361
|
+
if error_message
|
362
|
+
options[:error_messages] ||= {}
|
363
|
+
options[:error_messages][:invalid] = error_message
|
364
|
+
end
|
365
|
+
|
366
|
+
super(options)
|
284
367
|
|
285
|
-
|
368
|
+
@regex = regex
|
286
369
|
|
287
|
-
|
288
|
-
|
370
|
+
validators << Validators::RegexValidator.new(regex: regex)
|
371
|
+
end
|
289
372
|
end
|
290
|
-
end
|
291
373
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
374
|
+
class EmailField < CharField
|
375
|
+
def default_error_messages
|
376
|
+
super.merge(invalid: 'Enter a valid e-mail address.')
|
377
|
+
end
|
378
|
+
|
379
|
+
def default_validators
|
380
|
+
[Validators::ValidateEmail]
|
381
|
+
end
|
382
|
+
|
383
|
+
def clean(value)
|
384
|
+
value = to_object(value).strip
|
385
|
+
super(value)
|
386
|
+
end
|
303
387
|
end
|
304
388
|
|
305
|
-
|
306
|
-
|
389
|
+
# TODO: rewrite
|
390
|
+
class FileField < Field
|
391
|
+
def initialize(options)
|
392
|
+
@max_length = options.delete(:max_length)
|
393
|
+
@allow_empty_file = options.delete(:allow_empty_file)
|
394
|
+
super(options)
|
395
|
+
end
|
307
396
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
397
|
+
def default_error_messages
|
398
|
+
super.merge(invalid: 'No file was submitted. Check the encoding type on the form.',
|
399
|
+
missing: 'No file was submitted.',
|
400
|
+
empty: 'The submitted file is empty.',
|
401
|
+
max_length: 'Ensure this filename has at most %(max)d characters (it has %(length)d).',
|
402
|
+
contradiction: 'Please either submit a file or check the clear checkbox, not both.')
|
312
403
|
end
|
313
404
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
405
|
+
def default_widget
|
406
|
+
Widgets::ClearableFileInput
|
407
|
+
end
|
408
|
+
|
409
|
+
def to_object(data)
|
410
|
+
if Validators.empty_value?(data)
|
411
|
+
return nil
|
412
|
+
end
|
413
|
+
|
414
|
+
# UploadedFile objects should have name and size attributes.
|
415
|
+
begin
|
416
|
+
file_name = data.name
|
417
|
+
file_size = data.size
|
418
|
+
rescue NoMethodError
|
419
|
+
raise ValidationError.new(error_messages[:invalid])
|
420
|
+
end
|
421
|
+
|
422
|
+
if @max_length && file_name.length > @max_length
|
423
|
+
msg = Utils.format_string(error_messages[:max_length],
|
424
|
+
max: @max_length,
|
425
|
+
length: file_name.length)
|
426
|
+
raise ValidationError.new(msg)
|
427
|
+
end
|
428
|
+
|
429
|
+
if Utils.blank_value?(file_name)
|
430
|
+
raise ValidationError.new(error_messages[:invalid])
|
431
|
+
end
|
432
|
+
|
433
|
+
if !@allow_empty_file && !file_size
|
434
|
+
raise ValidationError.new(error_messages[:empty])
|
435
|
+
end
|
436
|
+
|
437
|
+
data
|
438
|
+
end
|
439
|
+
|
440
|
+
def clean(data, initial = nil)
|
441
|
+
# If the widget got contradictory inputs, we raise a validation error
|
442
|
+
if data.object_id == Widgets::ClearableFileInput::FILE_INPUT_CONTRADICTION.object_id
|
443
|
+
raise ValidationError.new(error_messages[:contradiction])
|
444
|
+
end
|
445
|
+
|
446
|
+
# False means the field value should be cleared; further validation is
|
447
|
+
# not needed.
|
448
|
+
if data == false
|
449
|
+
unless @required
|
450
|
+
return false
|
322
451
|
end
|
323
452
|
|
324
|
-
|
325
|
-
|
453
|
+
# If the field is required, clearing is not possible (the widget
|
454
|
+
# shouldn't return false data in that case anyway). false is not
|
455
|
+
# an 'empty_value'; if a false value makes it this far
|
456
|
+
# it should be validated from here on out as nil (so it will be
|
457
|
+
# caught by the required check).
|
458
|
+
data = nil
|
459
|
+
end
|
326
460
|
|
327
|
-
|
328
|
-
|
461
|
+
if !data && initial
|
462
|
+
initial
|
463
|
+
else
|
464
|
+
super(data)
|
329
465
|
end
|
466
|
+
end
|
330
467
|
|
331
|
-
data
|
468
|
+
def bound_data(data, initial)
|
469
|
+
if data.nil? || data.object_id == Widgets::ClearableFileInput::FILE_INPUT_CONTRADICTION.object_id
|
470
|
+
initial
|
471
|
+
else
|
472
|
+
data
|
473
|
+
end
|
474
|
+
end
|
332
475
|
end
|
333
|
-
end
|
334
476
|
|
335
|
-
|
336
|
-
|
477
|
+
#class ImageField < FileField
|
478
|
+
#end
|
337
479
|
|
338
|
-
|
480
|
+
# URLField
|
339
481
|
|
340
|
-
|
341
|
-
|
482
|
+
class BooleanField < Field
|
483
|
+
def default_widget
|
484
|
+
Widgets::CheckboxInput
|
485
|
+
end
|
342
486
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
end
|
487
|
+
def to_object(value)
|
488
|
+
if value.kind_of?(String) && ['false', '0'].include?(value.downcase)
|
489
|
+
value = false
|
490
|
+
else
|
491
|
+
value = Utils.make_bool(value)
|
492
|
+
end
|
350
493
|
|
351
|
-
|
352
|
-
widget Widgets::NullBooleanSelect
|
494
|
+
value = super(value)
|
353
495
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
496
|
+
if !value && required
|
497
|
+
raise ValidationError.new(error_messages[:required])
|
498
|
+
end
|
499
|
+
|
500
|
+
value
|
359
501
|
end
|
360
502
|
end
|
361
|
-
end
|
362
503
|
|
363
|
-
|
364
|
-
|
365
|
-
|
504
|
+
class NullBooleanField < BooleanField
|
505
|
+
def default_widget
|
506
|
+
Widgets::NullBooleanSelect
|
507
|
+
end
|
366
508
|
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
509
|
+
def to_object(value)
|
510
|
+
case value
|
511
|
+
when true, 'true', '1', 'on' then true
|
512
|
+
when false, 'false', '0' then false
|
513
|
+
else nil
|
514
|
+
end
|
515
|
+
end
|
372
516
|
|
373
|
-
|
374
|
-
|
517
|
+
def validate(value)
|
518
|
+
end
|
375
519
|
end
|
376
520
|
|
377
|
-
|
378
|
-
|
379
|
-
|
521
|
+
class ChoiceField < Field
|
522
|
+
def initialize(choices=[], options={})
|
523
|
+
options[:required] = options.fetch(:required, true)
|
524
|
+
super(options)
|
525
|
+
self.choices = choices
|
526
|
+
end
|
527
|
+
|
528
|
+
def initialize_copy(original)
|
529
|
+
super(original)
|
530
|
+
self.choices = original.choices.dup
|
531
|
+
end
|
380
532
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
value = value.to_s
|
533
|
+
def default_error_messages
|
534
|
+
super.merge(invalid_choice: 'Select a valid choice. %(value)s is not one of the available choices.')
|
535
|
+
end
|
385
536
|
|
386
|
-
|
537
|
+
def default_widget
|
538
|
+
Widgets::Select
|
539
|
+
end
|
387
540
|
|
388
|
-
|
389
|
-
|
541
|
+
def choices
|
542
|
+
@choices
|
543
|
+
end
|
544
|
+
|
545
|
+
def choices=(value)
|
546
|
+
@choices = @widget.choices = value
|
547
|
+
end
|
548
|
+
|
549
|
+
def to_object(value)
|
550
|
+
if Validators.empty_value?(value)
|
551
|
+
''
|
552
|
+
else
|
553
|
+
value.to_s
|
390
554
|
end
|
555
|
+
end
|
391
556
|
|
392
|
-
value
|
393
|
-
|
557
|
+
def validate(value)
|
558
|
+
super(value)
|
394
559
|
|
395
|
-
|
396
|
-
|
560
|
+
unless !value || Validators.empty_value?(value) || valid_value?(value)
|
561
|
+
msg = Utils.format_string(error_messages[:invalid_choice],
|
562
|
+
value: value)
|
563
|
+
raise ValidationError.new(msg)
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
def valid_value?(value)
|
568
|
+
@choices.each do |k, v|
|
397
569
|
if v.is_a?(Array)
|
398
570
|
# This is an optgroup, so look inside the group for options
|
399
571
|
v.each do |k2, v2|
|
400
572
|
return true if value == k2.to_s
|
401
573
|
end
|
574
|
+
elsif k.is_a?(Hash)
|
575
|
+
# this is a hash valued choice list
|
576
|
+
return true if value == k[:value].to_s
|
402
577
|
else
|
403
578
|
return true if value == k.to_s
|
404
579
|
end
|
405
580
|
end
|
406
|
-
false
|
407
|
-
end
|
408
|
-
end
|
409
581
|
|
410
|
-
|
411
|
-
|
412
|
-
def initialize(choices=[], options={})
|
413
|
-
@coerce = options.delete(:coerce) || lambda{|val| val}
|
414
|
-
@empty_value = options.fetch(:empty_value, '')
|
415
|
-
options.delete(:empty_value)
|
416
|
-
super(choices, options)
|
582
|
+
false
|
583
|
+
end
|
417
584
|
end
|
418
585
|
|
419
|
-
|
420
|
-
|
421
|
-
|
586
|
+
class TypedChoiceField < ChoiceField
|
587
|
+
def initialize(choices=[], options={})
|
588
|
+
@coerce = options.delete(:coerce) || lambda{|val| val}
|
589
|
+
@empty_value = options.fetch(:empty_value, '')
|
590
|
+
options.delete(:empty_value)
|
591
|
+
super(choices, options)
|
592
|
+
end
|
593
|
+
|
594
|
+
def to_object(value)
|
595
|
+
value = super(value)
|
596
|
+
original_validate(value)
|
422
597
|
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
598
|
+
if value == @empty_value || Validators.empty_value?(value)
|
599
|
+
return @empty_value
|
600
|
+
end
|
601
|
+
|
602
|
+
begin
|
603
|
+
@coerce.call(value)
|
604
|
+
rescue TypeError, ValidationError
|
605
|
+
msg = Utils.format_string(error_messages[:invalid_choice],
|
606
|
+
value: value)
|
607
|
+
raise ValidationError.new(msg)
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
alias_method :original_validate, :validate
|
612
|
+
|
613
|
+
def validate(value)
|
427
614
|
end
|
428
615
|
end
|
429
|
-
end
|
430
616
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
617
|
+
class MultipleChoiceField < ChoiceField
|
618
|
+
def default_error_messages
|
619
|
+
super.merge(invalid_choice: 'Select a valid choice. %(value)s is not one of the available choices.',
|
620
|
+
invalid_list: 'Enter a list of values.')
|
621
|
+
end
|
622
|
+
|
623
|
+
def default_widget
|
624
|
+
Widgets::SelectMultiple
|
625
|
+
end
|
626
|
+
|
627
|
+
def default_hidden_widget
|
628
|
+
Widgets::MultipleHiddenInput
|
629
|
+
end
|
630
|
+
|
631
|
+
def to_object(value)
|
632
|
+
if !value || Validators.empty_value?(value)
|
633
|
+
[]
|
634
|
+
elsif !value.is_a?(Array)
|
635
|
+
raise ValidationError.new(error_messages[:invalid_list])
|
636
|
+
else
|
637
|
+
value.map(&:to_s)
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
def validate(value)
|
642
|
+
if required && (!value || Validators.empty_value?(value))
|
643
|
+
raise ValidationError.new(error_messages[:required])
|
644
|
+
end
|
645
|
+
|
646
|
+
value.each do |val|
|
647
|
+
unless valid_value?(val)
|
648
|
+
msg = Utils.format_string(error_messages[:invalid_choice],
|
649
|
+
value: val)
|
650
|
+
raise ValidationError.new(msg)
|
448
651
|
end
|
449
652
|
end
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
# TypedMultipleChoiceField < MultipleChoiceField
|
657
|
+
|
658
|
+
# TODO: tests
|
659
|
+
class ComboField < Field
|
660
|
+
def initialize(fields=[], *args)
|
661
|
+
super(*args)
|
662
|
+
fields.each {|f| f.required = false}
|
663
|
+
@fields = fields
|
664
|
+
end
|
665
|
+
|
666
|
+
def clean(value)
|
667
|
+
super(value)
|
668
|
+
@fields.each {|f| value = f.clean(value)}
|
669
|
+
value
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
# MultiValueField
|
674
|
+
# FilePathField
|
675
|
+
# SplitDateTimeField
|
676
|
+
|
677
|
+
class IPAddressField < CharField
|
678
|
+
def default_error_messages
|
679
|
+
super.merge(invalid: 'Enter a valid IPv4 address.')
|
680
|
+
end
|
681
|
+
|
682
|
+
def default_validators
|
683
|
+
[Validators::ValidateIPV4Address]
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
class SlugField < CharField
|
688
|
+
def default_error_messages
|
689
|
+
super.merge(invalid: "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.")
|
690
|
+
end
|
450
691
|
|
451
|
-
|
692
|
+
def default_validators
|
693
|
+
[Validators::ValidateSlug]
|
694
|
+
end
|
452
695
|
end
|
453
696
|
end
|
454
|
-
|
455
|
-
# ComboField
|
456
|
-
# MultiValueField
|
457
|
-
# FilePathField
|
458
|
-
# SplitDateTimeField
|
459
|
-
# IPAddressField
|
460
|
-
# SlugField
|
461
|
-
end; end
|
697
|
+
end
|