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
@@ -1,64 +1,92 @@
|
|
1
|
-
require 'bureaucrat/fields'
|
2
|
-
|
3
1
|
module Bureaucrat
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
2
|
+
# Shortcuts for declaring form fields
|
3
|
+
module Quickfields
|
4
|
+
include Fields
|
5
|
+
|
6
|
+
# Hide field named +name+
|
7
|
+
def hide(name)
|
8
|
+
base_fields[name] = base_fields[name].dup
|
9
|
+
base_fields[name].widget = Widgets::HiddenInput.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# Delete field named +name+
|
13
|
+
def delete(name)
|
14
|
+
base_fields.delete name
|
15
|
+
end
|
16
|
+
|
17
|
+
# Declare a +CharField+ with text input widget
|
18
|
+
def string(name, options = {})
|
19
|
+
field name, CharField.new(options)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Declare a +CharField+ with text area widget
|
23
|
+
def text(name, options = {})
|
24
|
+
field name, CharField.new(options.merge(widget: Widgets::Textarea.new))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Declare a +CharField+ with password widget
|
28
|
+
def password(name, options = {})
|
29
|
+
field name, CharField.new(options.merge(widget: Widgets::PasswordInput.new))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Declare an +IntegerField+
|
33
|
+
def integer(name, options = {})
|
34
|
+
field name, IntegerField.new(options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Declare a +BigDecimalField+
|
38
|
+
def decimal(name, options = {})
|
39
|
+
field name, BigDecimalField.new(options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Declare a +RegexField+
|
43
|
+
def regex(name, regexp, options = {})
|
44
|
+
field name, RegexField.new(regexp, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Declare an +EmailField+
|
48
|
+
def email(name, options = {})
|
49
|
+
field name, EmailField.new(options)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Declare a +FileField+
|
53
|
+
def file(name, options = {})
|
54
|
+
field name, FileField.new(options)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Declare a +BooleanField+
|
58
|
+
def boolean(name, options = {})
|
59
|
+
field name, BooleanField.new(options)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Declare a +NullBooleanField+
|
63
|
+
def null_boolean(name, options = {})
|
64
|
+
field name, NullBooleanField.new(options)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Declare a +ChoiceField+ with +choices+
|
68
|
+
def choice(name, choices = [], options = {})
|
69
|
+
field name, ChoiceField.new(choices, options)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Declare a +TypedChoiceField+ with +choices+
|
73
|
+
def typed_choice(name, choices = [], options = {})
|
74
|
+
field name, TypedChoiceField.new(choices, options)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Declare a +MultipleChoiceField+ with +choices+
|
78
|
+
def multiple_choice(name, choices = [], options = {})
|
79
|
+
field name, MultipleChoiceField.new(choices, options)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Declare a +ChoiceField+ using the +RadioSelect+ widget
|
83
|
+
def radio_choice(name, choices = [], options = {})
|
84
|
+
field name, ChoiceField.new(choices, options.merge(widget: Widgets::RadioSelect.new))
|
85
|
+
end
|
86
|
+
|
87
|
+
# Declare a +MultipleChoiceField+ with the +CheckboxSelectMultiple+ widget
|
88
|
+
def checkbox_multiple_choice(name, choices = [], options = {})
|
89
|
+
field name, MultipleChoiceField.new(choices, options.merge(widget: Widgets::CheckboxSelectMultiple.new))
|
90
|
+
end
|
22
91
|
end
|
23
|
-
|
24
|
-
def integer(name, options={})
|
25
|
-
field name, IntegerField.new(options)
|
26
|
-
end
|
27
|
-
|
28
|
-
def decimal(name, options={})
|
29
|
-
field name, BigDecimalField.new(options)
|
30
|
-
end
|
31
|
-
|
32
|
-
def regex(name, regexp, options={})
|
33
|
-
field name, RegexField.new(regexp, options)
|
34
|
-
end
|
35
|
-
|
36
|
-
def email(name, options={})
|
37
|
-
field name, EmailField.new(options)
|
38
|
-
end
|
39
|
-
|
40
|
-
def file(name, options={})
|
41
|
-
field name, FileField.new(options)
|
42
|
-
end
|
43
|
-
|
44
|
-
def boolean(name, options={})
|
45
|
-
field name, BooleanField.new(options)
|
46
|
-
end
|
47
|
-
|
48
|
-
def null_boolean(name, options={})
|
49
|
-
field name, NullBooleanField.new(options)
|
50
|
-
end
|
51
|
-
|
52
|
-
def choice(name, choices=[], options={})
|
53
|
-
field name, ChoiceField.new(choices, options)
|
54
|
-
end
|
55
|
-
|
56
|
-
def typed_choice(name, choices=[], options={})
|
57
|
-
field name, TypedChoiceField.new(choices, options)
|
58
|
-
end
|
59
|
-
|
60
|
-
def multiple_choice(name, choices=[], options={})
|
61
|
-
field name, MultipleChoiceField.new(choices, options)
|
62
|
-
end
|
63
|
-
|
64
|
-
end; end
|
92
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Bureaucrat
|
2
|
+
class TemporaryUploadedFile
|
3
|
+
attr_accessor :filename, :content_type, :name, :tempfile, :head
|
4
|
+
|
5
|
+
def initialize(data)
|
6
|
+
@filename = data[:filename]
|
7
|
+
@content_type = data[:content_type]
|
8
|
+
@name = data[:name]
|
9
|
+
@tempfile = data[:tempfile]
|
10
|
+
@size = @tempfile.size
|
11
|
+
@head = data[:head]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/bureaucrat/utils.rb
CHANGED
@@ -1,84 +1,101 @@
|
|
1
1
|
module Bureaucrat
|
2
|
-
module Utils
|
2
|
+
module Utils
|
3
|
+
extend self
|
3
4
|
|
4
|
-
|
5
|
-
|
5
|
+
module SafeData
|
6
|
+
end
|
6
7
|
|
7
|
-
|
8
|
-
|
8
|
+
class SafeString < String
|
9
|
+
include SafeData
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
def +(rhs)
|
12
|
+
rhs.is_a?(SafeString) ? SafeString.new(super(rhs)) : super(rhs)
|
13
|
+
end
|
12
14
|
end
|
13
|
-
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
16
|
+
class StringAccessHash < Hash
|
17
|
+
def initialize(other = {})
|
18
|
+
super()
|
19
|
+
update(other)
|
20
|
+
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
22
|
+
def []=(key, value)
|
23
|
+
super(key.to_s, value)
|
24
|
+
end
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
end
|
26
|
+
def [](key)
|
27
|
+
super(key.to_s)
|
28
|
+
end
|
32
29
|
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
def fetch(key, *args)
|
31
|
+
super(key.to_s, *args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def include?(key)
|
35
|
+
super(key.to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
def update(other)
|
39
|
+
other.each_pair{|k, v| self[k] = v}
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def merge(other)
|
44
|
+
dup.update(other)
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(key)
|
48
|
+
super(key.to_s)
|
49
|
+
end
|
36
50
|
end
|
37
|
-
end
|
38
51
|
|
39
|
-
|
52
|
+
def blank_value?(value)
|
53
|
+
!value || value == ''
|
54
|
+
end
|
40
55
|
|
41
|
-
|
42
|
-
|
43
|
-
|
56
|
+
def mark_safe(s)
|
57
|
+
s.is_a?(SafeData) ? s : SafeString.new(s.to_s)
|
58
|
+
end
|
44
59
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
60
|
+
ESCAPES = {
|
61
|
+
'&' => '&',
|
62
|
+
'<' => '<',
|
63
|
+
'>' => '>',
|
64
|
+
'"' => '"',
|
65
|
+
"'" => '''
|
66
|
+
}
|
67
|
+
def escape(html)
|
68
|
+
mark_safe(html.gsub(/[&<>"']/) {|match| ESCAPES[match]})
|
69
|
+
end
|
55
70
|
|
56
|
-
|
57
|
-
|
58
|
-
|
71
|
+
def conditional_escape(html)
|
72
|
+
html.is_a?(SafeData) ? html : escape(html)
|
73
|
+
end
|
59
74
|
|
60
|
-
|
61
|
-
|
62
|
-
|
75
|
+
def flatatt(attrs)
|
76
|
+
attrs.map {|k, v| " #{k}=\"#{conditional_escape(v)}\""}.join('')
|
77
|
+
end
|
63
78
|
|
64
|
-
|
65
|
-
|
66
|
-
|
79
|
+
def format_string(string, values)
|
80
|
+
output = string.dup
|
81
|
+
values.each_pair do |variable, value|
|
67
82
|
output.gsub!(/%\(#{variable}\)s/, value.to_s)
|
68
83
|
end
|
69
|
-
|
70
|
-
|
84
|
+
output
|
85
|
+
end
|
71
86
|
|
72
|
-
|
73
|
-
|
74
|
-
|
87
|
+
def pretty_name(name)
|
88
|
+
name.to_s.capitalize.gsub(/_/, ' ')
|
89
|
+
end
|
75
90
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
91
|
+
def make_float(value)
|
92
|
+
value += '0' if value.is_a?(String) && value != '.' && value[-1,1] == '.'
|
93
|
+
Float(value)
|
94
|
+
end
|
95
|
+
|
96
|
+
def make_bool(value)
|
97
|
+
!(value.respond_to?(:empty?) ? value.empty? : [0, nil, false].include?(value))
|
98
|
+
end
|
80
99
|
|
81
|
-
def make_bool(value)
|
82
|
-
!(value.respond_to?(:empty?) ? value.empty? : [0, nil, false].include?(value))
|
83
100
|
end
|
84
|
-
end
|
101
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Bureaucrat
|
2
|
+
module Validators
|
3
|
+
def empty_value?(value)
|
4
|
+
value.nil? || value == '' || value == [] || value == {}
|
5
|
+
end
|
6
|
+
module_function :empty_value?
|
7
|
+
|
8
|
+
class RegexValidator
|
9
|
+
attr_accessor :regex, :message, :code
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@regex = Regexp.new(options.fetch(:regex, ''))
|
13
|
+
@message = options.fetch(:message, 'Enter a valid value.')
|
14
|
+
@code = options.fetch(:code, :invalid)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Validates that the input validates the regular expression
|
18
|
+
def call(value)
|
19
|
+
if regex !~ value
|
20
|
+
raise ValidationError.new(@message, code, regex: regex)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
ValidateInteger = lambda do |value|
|
26
|
+
begin
|
27
|
+
Integer(value)
|
28
|
+
rescue ArgumentError
|
29
|
+
raise ValidationError.new('')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Original from Django's EmailField:
|
34
|
+
# email_re = re.compile(
|
35
|
+
# r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
|
36
|
+
# r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' # quoted-string
|
37
|
+
# r')@(?:[A-Z0-9]+(?:-*[A-Z0-9]+)*\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain
|
38
|
+
EMAIL_RE = /
|
39
|
+
(^[-!#\$%&'*+\/=?^_`{}|~0-9A-Z]+(\.[-!#\$%&'*+\/=?^_`{}|~0-9A-Z]+)*
|
40
|
+
|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"
|
41
|
+
)@(?:[A-Z0-9]+(?:-*[A-Z0-9]+)*\.)+[A-Z]{2,6}$
|
42
|
+
/xi
|
43
|
+
|
44
|
+
ValidateEmail =
|
45
|
+
RegexValidator.new(regex: EMAIL_RE,
|
46
|
+
message: 'Enter a valid e-mail address.')
|
47
|
+
|
48
|
+
SLUG_RE = /^[-\w]+$/
|
49
|
+
|
50
|
+
ValidateSlug =
|
51
|
+
RegexValidator.new(regex: SLUG_RE,
|
52
|
+
message: "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.")
|
53
|
+
|
54
|
+
IPV4_RE = /^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$/
|
55
|
+
|
56
|
+
IPV4Validator =
|
57
|
+
RegexValidator.new(regex: IPV4_RE,
|
58
|
+
message: 'Enter a valid IPv4 address.')
|
59
|
+
|
60
|
+
COMMA_SEPARATED_INT_LIST_RE = /^[\d,]+$/
|
61
|
+
|
62
|
+
ValidateCommaSeparatedIntegerList =
|
63
|
+
RegexValidator.new(regex: COMMA_SEPARATED_INT_LIST_RE,
|
64
|
+
message: 'Enter only digits separated by commas.',
|
65
|
+
code: :invalid)
|
66
|
+
|
67
|
+
class BaseValidator
|
68
|
+
def initialize(limit_value)
|
69
|
+
@limit_value = limit_value
|
70
|
+
end
|
71
|
+
|
72
|
+
def message
|
73
|
+
'Ensure this value is %(limit_value)s (it is %(show_value)s).'
|
74
|
+
end
|
75
|
+
|
76
|
+
def code
|
77
|
+
:limit_value
|
78
|
+
end
|
79
|
+
|
80
|
+
def compare(a, b)
|
81
|
+
a.object_id != b.object_id
|
82
|
+
end
|
83
|
+
|
84
|
+
def clean(x)
|
85
|
+
x
|
86
|
+
end
|
87
|
+
|
88
|
+
def call(value)
|
89
|
+
cleaned = clean(value)
|
90
|
+
params = { limit_value: @limit_value, show_value: cleaned }
|
91
|
+
|
92
|
+
if compare(cleaned, @limit_value)
|
93
|
+
msg = Utils.format_string(message, params)
|
94
|
+
raise ValidationError.new(msg, code, params)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class MaxValueValidator < BaseValidator
|
100
|
+
def message
|
101
|
+
'Ensure this value is less than or equal to %(limit_value)s.'
|
102
|
+
end
|
103
|
+
|
104
|
+
def code
|
105
|
+
:max_value
|
106
|
+
end
|
107
|
+
|
108
|
+
def compare(a, b)
|
109
|
+
a > b
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class MinValueValidator < BaseValidator
|
114
|
+
def message
|
115
|
+
'Ensure this value is greater than or equal to %(limit_value)s.'
|
116
|
+
end
|
117
|
+
|
118
|
+
def code
|
119
|
+
:min_value
|
120
|
+
end
|
121
|
+
|
122
|
+
def compare(a, b)
|
123
|
+
a < b
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class MinLengthValidator < BaseValidator
|
128
|
+
def message
|
129
|
+
'Ensure this value has at least %(limit_value)d characters (it has %(show_value)d).'
|
130
|
+
end
|
131
|
+
|
132
|
+
def code
|
133
|
+
:min_length
|
134
|
+
end
|
135
|
+
|
136
|
+
def compare(a, b)
|
137
|
+
a < b
|
138
|
+
end
|
139
|
+
|
140
|
+
def clean(x)
|
141
|
+
x.length
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class MaxLengthValidator < BaseValidator
|
146
|
+
def message
|
147
|
+
'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).'
|
148
|
+
end
|
149
|
+
|
150
|
+
def code
|
151
|
+
:max_length
|
152
|
+
end
|
153
|
+
|
154
|
+
def compare(a, b)
|
155
|
+
a > b
|
156
|
+
end
|
157
|
+
|
158
|
+
def clean(x)
|
159
|
+
x.length
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|