fulcrum 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|