bureaucrat 0.0.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|