fulcrum 0.1.2
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/.gitignore +17 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +123 -0
- data/Rakefile +8 -0
- data/fulcrum.gemspec +27 -0
- data/lib/fulcrum.rb +13 -0
- data/lib/fulcrum/api.rb +95 -0
- data/lib/fulcrum/choice_list.rb +38 -0
- data/lib/fulcrum/classification_set.rb +39 -0
- data/lib/fulcrum/form.rb +39 -0
- data/lib/fulcrum/member.rb +16 -0
- data/lib/fulcrum/photo.rb +36 -0
- data/lib/fulcrum/record.rb +38 -0
- data/lib/fulcrum/validators/base_validator.rb +31 -0
- data/lib/fulcrum/validators/choice_list_validator.rb +30 -0
- data/lib/fulcrum/validators/classification_set_validator.rb +38 -0
- data/lib/fulcrum/validators/form_validator.rb +150 -0
- data/lib/fulcrum/validators/record_validator.rb +18 -0
- data/lib/fulcrum/version.rb +3 -0
- data/spec/data/form_data.json +175 -0
- data/spec/data/test.jpg +0 -0
- data/spec/lib/api_spec.rb +39 -0
- data/spec/lib/choice_list_spec.rb +120 -0
- data/spec/lib/classification_set_spec.rb +119 -0
- data/spec/lib/form_spec.rb +120 -0
- data/spec/lib/member_spec.rb +52 -0
- data/spec/lib/photo_spec.rb +94 -0
- data/spec/lib/record_spec.rb +120 -0
- data/spec/lib/validators/choice_list_validator_spec.rb +73 -0
- data/spec/lib/validators/classification_set_validator_spec.rb +88 -0
- data/spec/lib/validators/form_validator_spec.rb +4 -0
- data/spec/lib/validators/record_validator_spec.rb +53 -0
- data/spec/spec_helper.rb +5 -0
- metadata +182 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module Fulcrum
|
2
|
+
class Photo < Api
|
3
|
+
|
4
|
+
ALLOWED_FORMATS = %w(json image)
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def find(id, opts = {})
|
9
|
+
format = image_opts(opts)
|
10
|
+
call(:get, "photos/#{id}.#{format}")
|
11
|
+
end
|
12
|
+
|
13
|
+
def thumbnail(id, opts = {})
|
14
|
+
format = image_opts(opts)
|
15
|
+
call(:get, "photos/#{id}/thumbnail.#{format}")
|
16
|
+
end
|
17
|
+
|
18
|
+
def create(file, content_type, id, label = '')
|
19
|
+
photo = Faraday::UploadIO.new(file, content_type)
|
20
|
+
call(:post, 'photos.json', { photo: { file: photo, access_key: id, label: label}})
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(id)
|
24
|
+
call(:delete, "photos/#{id}.json")
|
25
|
+
end
|
26
|
+
|
27
|
+
def image_opts(opts = {})
|
28
|
+
opts = opts.with_indifferent_access
|
29
|
+
format = opts.delete(:format) || 'json'
|
30
|
+
raise ArgumentError, "#{format} is not an allowed format, use either 'json' or 'image'" unless ALLOWED_FORMATS.include?(format)
|
31
|
+
format = "jpg" if format == 'image'
|
32
|
+
format
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Fulcrum
|
2
|
+
class Record < Api
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def all(opts = {})
|
7
|
+
params = parse_opts([:page, :form_id, :bounding_box, :updated_since], opts)
|
8
|
+
call(:get, 'records.json', params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(id)
|
12
|
+
call(:get, "records/#{id}.json")
|
13
|
+
end
|
14
|
+
|
15
|
+
def create(record)
|
16
|
+
validation = RecordValidator.new(record)
|
17
|
+
if validation.valid?
|
18
|
+
call(:post, 'records.json', record)
|
19
|
+
else
|
20
|
+
{ error: { validation: validation.errors } }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def update(id, record)
|
25
|
+
validation = RecordValidator.new(record)
|
26
|
+
if validation.valid?
|
27
|
+
call(:put, "records/#{id}.json", record)
|
28
|
+
else
|
29
|
+
{ error: { validation: validation.errors } }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete(id)
|
34
|
+
call(:delete, "records/#{id}.json")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'active_support/core_ext/hash'
|
2
|
+
|
3
|
+
module Fulcrum
|
4
|
+
class BaseValidator
|
5
|
+
attr_accessor :data
|
6
|
+
attr_accessor :errors
|
7
|
+
|
8
|
+
def initialize(data)
|
9
|
+
@data = (data.is_a?(Hash) ? data : JSON.parse(data)).with_indifferent_access
|
10
|
+
@errors = {}
|
11
|
+
validate!
|
12
|
+
end
|
13
|
+
|
14
|
+
def valid?
|
15
|
+
errors.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_error(key, data_name, error)
|
19
|
+
if errors.has_key?(key)
|
20
|
+
if errors[key].has_key?(data_name)
|
21
|
+
errors[key][data_name].push(error)
|
22
|
+
else
|
23
|
+
errors[key][data_name] = [error]
|
24
|
+
end
|
25
|
+
else
|
26
|
+
errors[key] = {}
|
27
|
+
errors[key][data_name] = [error]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Fulcrum
|
2
|
+
class ChoiceListValidator < BaseValidator
|
3
|
+
|
4
|
+
def validate!
|
5
|
+
if data['choice_list'].kind_of?(Hash) && !data['choice_list'].blank?
|
6
|
+
add_error('choice_list', 'name', 'must not be blank') if data['choice_list']['name'].blank?
|
7
|
+
choices(data['choice_list']['choices'])
|
8
|
+
else
|
9
|
+
@errors['choice_list'] = ['must be a non-empty hash']
|
10
|
+
end
|
11
|
+
return valid?
|
12
|
+
end
|
13
|
+
|
14
|
+
def choices(elements)
|
15
|
+
if elements.kind_of?(Array) && !elements.empty?
|
16
|
+
elements.each do |choice|
|
17
|
+
if choice.blank?
|
18
|
+
add_error('choices', 'choice', 'cannot be empty')
|
19
|
+
else
|
20
|
+
if choice['label'].blank?
|
21
|
+
add_error('choice', 'label', 'is required')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
else
|
26
|
+
add_error('choice_list', 'choices', 'must be a non-empty array')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Fulcrum
|
2
|
+
class ClassificationSetValidator < BaseValidator
|
3
|
+
|
4
|
+
def validate!
|
5
|
+
if data['classification_set']
|
6
|
+
add_error('classification_set', 'name', 'cannot be blank') if data['classification_set']['name'].blank?
|
7
|
+
|
8
|
+
if data['classification_set']['items'].blank? || !data['classification_set']['items'].kind_of?(Array)
|
9
|
+
add_error('classification_set', 'items', 'must be a non-empty array')
|
10
|
+
else
|
11
|
+
items(data['classification_set']['items'])
|
12
|
+
end
|
13
|
+
else
|
14
|
+
@errors['classification_set'] = ['must exist and not be blank']
|
15
|
+
end
|
16
|
+
return valid?
|
17
|
+
end
|
18
|
+
|
19
|
+
def items(elements, child = false)
|
20
|
+
parent, child = child ? ['child_classification', 'item'] : ['items', 'item']
|
21
|
+
if elements.kind_of?(Array) && !elements.empty?
|
22
|
+
elements.each do |element|
|
23
|
+
if element.blank?
|
24
|
+
add_error(parent, child, 'cannot be empty')
|
25
|
+
else
|
26
|
+
if element['label'].blank?
|
27
|
+
add_error(child, 'label', 'is required')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
items(element['child_classifications'], true) if element.has_key?('child_classifications')
|
31
|
+
end
|
32
|
+
else
|
33
|
+
add_error(parent, child, 'must be a non-empty array')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Fulcrum
|
2
|
+
class FormValidator < BaseValidator
|
3
|
+
|
4
|
+
TYPES = %w(
|
5
|
+
TextField
|
6
|
+
ChoiceField
|
7
|
+
ClassificationField
|
8
|
+
PhotoField
|
9
|
+
DateTimeField
|
10
|
+
Section
|
11
|
+
)
|
12
|
+
|
13
|
+
def form_elements
|
14
|
+
data['form']['elements']
|
15
|
+
end
|
16
|
+
|
17
|
+
def form_name
|
18
|
+
data['form']['name']
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate!
|
22
|
+
@items = {}
|
23
|
+
if data[:form]
|
24
|
+
if form_elements && !form_elements.empty?
|
25
|
+
add_error('form', 'name', 'cannot be blank') if data[:form][:name].blank?
|
26
|
+
fields(form_elements)
|
27
|
+
conditionals(form_elements)
|
28
|
+
else
|
29
|
+
add_error('form', 'elements', 'must be a non-empty array')
|
30
|
+
end
|
31
|
+
else
|
32
|
+
@errors['form'] = ['must exist and not be empty']
|
33
|
+
end
|
34
|
+
return valid?
|
35
|
+
end
|
36
|
+
|
37
|
+
def fields(elements)
|
38
|
+
if elements.kind_of?(Array) && !elements.empty?
|
39
|
+
elements.each { |element| field(element) }
|
40
|
+
else
|
41
|
+
add_error('form', 'elements', 'must be a non-empty array')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def field(element)
|
46
|
+
if element.blank?
|
47
|
+
add_error('elements', 'element', 'must not be empty')
|
48
|
+
else
|
49
|
+
if !element[:key]
|
50
|
+
add_error('element', 'key', 'must exist and not be nil')
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
|
54
|
+
if @items.include?(element[:key])
|
55
|
+
add_error(element[:key], :key, 'must be unique')
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
|
59
|
+
key = element[:key]
|
60
|
+
@items[key] = element[:type]
|
61
|
+
|
62
|
+
add_error(key, 'label', 'is required') if element[:label].blank?
|
63
|
+
add_error(key, 'data_name', 'is required') if element[:data_name].blank?
|
64
|
+
add_error(key, 'type', 'is not one of the valid types') unless TYPES.include?(element[:type])
|
65
|
+
|
66
|
+
%w(disabled hidden required).each do |attrib|
|
67
|
+
add_error(key, attrib, 'must be true or false') unless [true, false].include?(element[attrib])
|
68
|
+
end
|
69
|
+
|
70
|
+
case element[:type]
|
71
|
+
|
72
|
+
when 'ClassificationField'
|
73
|
+
if element[:classification_set_id]
|
74
|
+
add_error(key, 'classification_set_id', 'is required') if element[:classification_set_id].blank?
|
75
|
+
end
|
76
|
+
|
77
|
+
when 'Section'
|
78
|
+
if element['elements'].is_a?(Array)
|
79
|
+
if element['elements'].any?
|
80
|
+
fields(element['elements'])
|
81
|
+
else
|
82
|
+
add_error(key, 'elements', 'must contain additional elements')
|
83
|
+
end
|
84
|
+
else
|
85
|
+
add_error(key, 'elements', 'must be an array object')
|
86
|
+
end
|
87
|
+
|
88
|
+
when 'ChoiceField'
|
89
|
+
if element['choice_list_id']
|
90
|
+
add_error(key, 'choice_list_id', 'is required') if element[:choice_list_id].blank?
|
91
|
+
else
|
92
|
+
if element['choices'].is_a?(Array) && !element['choices'].blank?
|
93
|
+
element['choices'].each do |choice|
|
94
|
+
unless choice.has_key?('label') && choice['label'].present?
|
95
|
+
add_error('choices', 'label', 'contains an invalid label')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
else
|
99
|
+
add_error(key, 'choices', 'must be a non-empty array')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def conditionals(elements)
|
107
|
+
elements.each { |element| conditional(element) }
|
108
|
+
end
|
109
|
+
|
110
|
+
def conditional(element)
|
111
|
+
|
112
|
+
operators = case element[:type]
|
113
|
+
when 'ChoiceField', 'ClassificationField'
|
114
|
+
%w(equal_to not_equal_to is_empty is_not_empty)
|
115
|
+
else
|
116
|
+
%w(equal_to not_equal_to contains starts_with greater_than less_than is_empty is_not_empty)
|
117
|
+
end
|
118
|
+
|
119
|
+
%w(required_conditions visible_conditions).each do |field|
|
120
|
+
|
121
|
+
if type = element["#{field}_type"]
|
122
|
+
add_error(key, "#{field}_type", 'is not valid') unless %(any all).include?(type)
|
123
|
+
end
|
124
|
+
|
125
|
+
if element[field]
|
126
|
+
if element[field].is_a?(Array)
|
127
|
+
element[field].each do |condition|
|
128
|
+
|
129
|
+
if key = condition[:field_key]
|
130
|
+
add_error(key, field, "key #{key} does not exist on the form") unless @items.keys.include?(key)
|
131
|
+
add_error(key, field, "operator for #{key} is invalid") unless operators.include?(condition[:operator])
|
132
|
+
|
133
|
+
if %w(is_empty is_not_empty).include?(condition[:operator]) && condition[:value].present?
|
134
|
+
add_error(key, field, 'value cannot be blank')
|
135
|
+
end
|
136
|
+
else
|
137
|
+
add_error(key, field, 'field key must exist for condition')
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
else
|
142
|
+
add_error(key, field, 'must be an array object')
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
conditionals(element[:elements]) if element[:type] == 'Section'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Fulcrum
|
2
|
+
class RecordValidator < BaseValidator
|
3
|
+
def validate!
|
4
|
+
if data['record'].kind_of?(Hash) && !data['record'].empty?
|
5
|
+
add_error('record', 'form_id', 'cannot be blank') if data['record']['form_id'].blank?
|
6
|
+
if data['record']['latitude'].to_f == 0.0 || data['record']['longitude'] == 0.0
|
7
|
+
add_error('record', 'coordinates', 'must be in WGS84 format')
|
8
|
+
end
|
9
|
+
if !data['record']['form_values'].kind_of?(Hash) || data['record']['form_values'].blank?
|
10
|
+
add_error('record', 'form_values', 'must be a non-empty hash')
|
11
|
+
end
|
12
|
+
else
|
13
|
+
@errors['record'] = ['must be a non-empty hash']
|
14
|
+
end
|
15
|
+
return valid?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
{
|
2
|
+
"form": {
|
3
|
+
"name": "Park Maintinence",
|
4
|
+
"description": "Form for inspection of park's state of maintenance.",
|
5
|
+
"created_at": "2012-04-27T10:48:31-04:00",
|
6
|
+
"updated_at": "2012-04-27T10:58:39-04:00",
|
7
|
+
"elements": [
|
8
|
+
{
|
9
|
+
"disabled":false,
|
10
|
+
"hidden":false,
|
11
|
+
"key":"b8fbca33-cad6-1eb5-b4ec-d78cf8aa9323",
|
12
|
+
"type":"Section",
|
13
|
+
"data_name":"park_report",
|
14
|
+
"required":false,
|
15
|
+
"label":"Park Report",
|
16
|
+
"elements":[
|
17
|
+
{
|
18
|
+
"disabled":false,
|
19
|
+
"hidden":false,
|
20
|
+
"multiple":false,
|
21
|
+
"allow_other":true,
|
22
|
+
"key":"42277b35-92a0-2fe5-15df-baf4a2c11d69",
|
23
|
+
"type":"ChoiceField",
|
24
|
+
"data_name":"amenity_type",
|
25
|
+
"required":false,
|
26
|
+
"label":"Amenity Type",
|
27
|
+
"choices":[
|
28
|
+
{
|
29
|
+
"value":"Boardwalk",
|
30
|
+
"label":"Boardwalk"
|
31
|
+
},
|
32
|
+
{
|
33
|
+
"value":"Pavilion",
|
34
|
+
"label":"Pavilion"
|
35
|
+
},
|
36
|
+
{
|
37
|
+
"value":"Bench",
|
38
|
+
"label":"Bench"
|
39
|
+
},
|
40
|
+
{
|
41
|
+
"value":"Path",
|
42
|
+
"label":"Path"
|
43
|
+
},
|
44
|
+
{
|
45
|
+
"value":"Playground Equipment",
|
46
|
+
"label":"Playground Equipment"
|
47
|
+
},
|
48
|
+
{
|
49
|
+
"value":"Land",
|
50
|
+
"label":"Land"
|
51
|
+
},
|
52
|
+
{
|
53
|
+
"value":"Restroom",
|
54
|
+
"label":"Restroom"
|
55
|
+
},
|
56
|
+
{
|
57
|
+
"value":"Athletic Field",
|
58
|
+
"label":"Athletic Field"
|
59
|
+
},
|
60
|
+
{
|
61
|
+
"value":"Boat Ramp",
|
62
|
+
"label":"Boat Ramp"
|
63
|
+
},
|
64
|
+
{
|
65
|
+
"value":"Picnic Table",
|
66
|
+
"label":"Picnic Table"
|
67
|
+
},
|
68
|
+
{
|
69
|
+
"value":"Dog Park",
|
70
|
+
"label":"Dog Park"
|
71
|
+
}
|
72
|
+
]
|
73
|
+
},
|
74
|
+
{
|
75
|
+
"disabled":false,
|
76
|
+
"hidden":false,
|
77
|
+
"multiple":false,
|
78
|
+
"allow_other":true,
|
79
|
+
"key":"dec7d431-981e-55fe-ce27-fa0ce588b7b3",
|
80
|
+
"type":"ChoiceField",
|
81
|
+
"data_name":"type_of_problem",
|
82
|
+
"required":false,
|
83
|
+
"label":"Type of Problem",
|
84
|
+
"choices":[
|
85
|
+
{
|
86
|
+
"value":"Vandalism",
|
87
|
+
"label":"Vandalism"
|
88
|
+
},
|
89
|
+
{
|
90
|
+
"value":"Invasive Plant Species",
|
91
|
+
"label":"Invasive Plant Species"
|
92
|
+
},
|
93
|
+
{
|
94
|
+
"value":"Pests",
|
95
|
+
"label":"Pests"
|
96
|
+
},
|
97
|
+
{
|
98
|
+
"value":"Damage",
|
99
|
+
"label":"Damage"
|
100
|
+
},
|
101
|
+
{
|
102
|
+
"value":"None",
|
103
|
+
"label":"None"
|
104
|
+
}
|
105
|
+
]
|
106
|
+
},
|
107
|
+
{
|
108
|
+
"disabled":false,
|
109
|
+
"hidden":false,
|
110
|
+
"key":"bbd86b27-4792-1ba3-6bf3-65b7bb5ee124",
|
111
|
+
"type":"TextField",
|
112
|
+
"data_name":"problem_notes",
|
113
|
+
"required":false,
|
114
|
+
"label":"Problem Notes"
|
115
|
+
},
|
116
|
+
{
|
117
|
+
"disabled":false,
|
118
|
+
"hidden":false,
|
119
|
+
"multiple":false,
|
120
|
+
"allow_other":true,
|
121
|
+
"key":"36d58746-2b5c-a545-3009-5744de326e69",
|
122
|
+
"type":"ChoiceField",
|
123
|
+
"data_name":"urgency_",
|
124
|
+
"required":false,
|
125
|
+
"label":"Urgency ",
|
126
|
+
"choices":[
|
127
|
+
{
|
128
|
+
"value":"High",
|
129
|
+
"label":"High"
|
130
|
+
},
|
131
|
+
{
|
132
|
+
"value":"Medium",
|
133
|
+
"label":"Medium"
|
134
|
+
},
|
135
|
+
{
|
136
|
+
"value":"Low",
|
137
|
+
"label":"Low"
|
138
|
+
},
|
139
|
+
{
|
140
|
+
"value":"None",
|
141
|
+
"label":"None"
|
142
|
+
}
|
143
|
+
]
|
144
|
+
},
|
145
|
+
{
|
146
|
+
"disabled":false,
|
147
|
+
"hidden":false,
|
148
|
+
"key":"4194cc93-99ff-fb2c-ec1f-ad4e394e6731",
|
149
|
+
"type":"TextField",
|
150
|
+
"data_name":"name_of_park",
|
151
|
+
"required":false,
|
152
|
+
"label":"Name of Park"
|
153
|
+
},
|
154
|
+
{
|
155
|
+
"disabled":false,
|
156
|
+
"hidden":false,
|
157
|
+
"key":"21f0b6d5-4154-030e-d552-13819dfb3a0a",
|
158
|
+
"type":"PhotoField",
|
159
|
+
"required":false,
|
160
|
+
"label":"Photos"
|
161
|
+
}
|
162
|
+
]
|
163
|
+
},
|
164
|
+
{
|
165
|
+
"disabled":false,
|
166
|
+
"hidden":false,
|
167
|
+
"key":"1797370a-06c3-a263-6c8f-17dcf7186b11",
|
168
|
+
"type":"DateTimeField",
|
169
|
+
"data_name":"date_of_report",
|
170
|
+
"required":false,
|
171
|
+
"label":"Date of Report"
|
172
|
+
}
|
173
|
+
]
|
174
|
+
}
|
175
|
+
}
|