open_rosa 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +285 -0
- data/Rakefile +12 -0
- data/examples/authentication.rb +80 -0
- data/examples/rack_app.rb +178 -0
- data/examples/rails_integration.rb +152 -0
- data/lib/open_rosa/field_context.rb +59 -0
- data/lib/open_rosa/fields/base.rb +18 -0
- data/lib/open_rosa/fields/boolean.rb +17 -0
- data/lib/open_rosa/fields/group.rb +19 -0
- data/lib/open_rosa/fields/input.rb +32 -0
- data/lib/open_rosa/fields/range.rb +34 -0
- data/lib/open_rosa/fields/repeat.rb +20 -0
- data/lib/open_rosa/fields/select.rb +28 -0
- data/lib/open_rosa/fields/select1.rb +27 -0
- data/lib/open_rosa/fields/trigger.rb +17 -0
- data/lib/open_rosa/fields/upload.rb +18 -0
- data/lib/open_rosa/form.rb +146 -0
- data/lib/open_rosa/form_dsl.rb +70 -0
- data/lib/open_rosa/form_list.rb +97 -0
- data/lib/open_rosa/manifest.rb +105 -0
- data/lib/open_rosa/media_file.rb +97 -0
- data/lib/open_rosa/middleware.rb +352 -0
- data/lib/open_rosa/submission.rb +195 -0
- data/lib/open_rosa/version.rb +5 -0
- data/lib/open_rosa/xform.rb +250 -0
- data/lib/open_rosa.rb +27 -0
- data/sig/openrosa.rbs +4 -0
- metadata +105 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4971029b8b1f9034f894d18db290b204f7e4a8a62d7955dd9ecc9bf23a2669fc
|
|
4
|
+
data.tar.gz: 3dc882cc5e3b522d920c69c0b18d82fb6c8d4a2b0e44ecd41fc18155487f1efc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b90bcba3f90852d450de4569d26a3e3ffff4b4e67e7a879ad4115f638ee3871033d3f92e0dd18ad810b33255b54e9ab8f4142c34e24a75c5dfcb42dd6dfc3dc3
|
|
7
|
+
data.tar.gz: bb75af6367be14823f22259ddaff589f48b6a3f33794c0874633182741080cd2502d02bb87242520f71ca3486a7e8ddf28b8aa11f12c811ee706c32d454c76b6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2025-12-26
|
|
11
|
+
|
|
12
|
+
Initial release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Form DSL for defining OpenRosa forms with metadata (`form_id`, `version`, `name`, `description_text`, `download_url`, `manifest_url`)
|
|
17
|
+
- Field types: `input`, `select1`, `select`, `boolean`, `range`, `upload`, `trigger`, `group`, `repeat`
|
|
18
|
+
- XForm XML generation from form definitions
|
|
19
|
+
- Form List API for listing available forms
|
|
20
|
+
- Manifest support for media files
|
|
21
|
+
- Submission parsing with support for XML data and file attachments
|
|
22
|
+
- Rack middleware for serving OpenRosa endpoints (`/formList`, `/forms/:id`, `/submission`)
|
|
23
|
+
- Per-form and global submission handlers
|
|
24
|
+
- Authentication hook support
|
|
25
|
+
- Ruby 3.2+ compatibility
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Before finishing a task, run `rake` to make sure that tests and Rubocop is happy. Fix as many Rubocop issues as you can. If you can't, use the comment syntax to disable the rule.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alex Watt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# OpenRosa Ruby
|
|
2
|
+
|
|
3
|
+
A Ruby gem for building [OpenRosa](https://docs.getodk.org/openrosa/) compliant form servers. Define forms with a clean DSL, serve them via Rack middleware, and handle submissions from mobile data collection apps like ODK Collect.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "open_rosa"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install open_rosa
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Define a Form
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class SurveyForm < OpenRosa::Form
|
|
25
|
+
form_id "my_survey"
|
|
26
|
+
version "1.0.0"
|
|
27
|
+
name "Customer Survey"
|
|
28
|
+
|
|
29
|
+
input :name, label: "Your Name", type: :string, required: true
|
|
30
|
+
input :email, label: "Email", type: :string
|
|
31
|
+
input :age, label: "Age", type: :int
|
|
32
|
+
|
|
33
|
+
select1 :satisfaction,
|
|
34
|
+
label: "How satisfied are you?",
|
|
35
|
+
choices: {
|
|
36
|
+
"very_satisfied" => "Very Satisfied",
|
|
37
|
+
"satisfied" => "Satisfied",
|
|
38
|
+
"neutral" => "Neutral",
|
|
39
|
+
"dissatisfied" => "Dissatisfied"
|
|
40
|
+
},
|
|
41
|
+
required: true
|
|
42
|
+
|
|
43
|
+
boolean :would_recommend, label: "Would you recommend us?"
|
|
44
|
+
|
|
45
|
+
input :comments, label: "Additional Comments", type: :string
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Mount the Middleware
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# config.ru (Rack) or config/application.rb (Rails)
|
|
53
|
+
use OpenRosa::Middleware do |config|
|
|
54
|
+
config.base_url = "https://example.com"
|
|
55
|
+
config.mount_path = "/openrosa"
|
|
56
|
+
config.forms = [SurveyForm]
|
|
57
|
+
|
|
58
|
+
config.on_submission do |submission|
|
|
59
|
+
puts "Received: #{submission.form_id}"
|
|
60
|
+
puts "Data: #{submission.data}"
|
|
61
|
+
"Thank you for your submission!"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Available Endpoints
|
|
67
|
+
|
|
68
|
+
The middleware provides these OpenRosa-compliant endpoints:
|
|
69
|
+
|
|
70
|
+
| Method | Path | Description |
|
|
71
|
+
|--------|------|-------------|
|
|
72
|
+
| GET | `/openrosa/formList` | List available forms |
|
|
73
|
+
| GET | `/openrosa/forms/:id` | Download form XForm XML |
|
|
74
|
+
| HEAD | `/openrosa/submission` | Pre-flight check |
|
|
75
|
+
| POST | `/openrosa/submission` | Submit form data |
|
|
76
|
+
|
|
77
|
+
## Field Types
|
|
78
|
+
|
|
79
|
+
### input
|
|
80
|
+
|
|
81
|
+
Text, numeric, and date inputs:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
input :name, label: "Name", type: :string, required: true
|
|
85
|
+
input :age, label: "Age", type: :int
|
|
86
|
+
input :temperature, label: "Temperature", type: :decimal
|
|
87
|
+
input :birthdate, label: "Birth Date", type: :date
|
|
88
|
+
input :appointment, label: "Appointment", type: :dateTime
|
|
89
|
+
input :start_time, label: "Start Time", type: :time
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### select1
|
|
93
|
+
|
|
94
|
+
Single-choice selection:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
select1 :color,
|
|
98
|
+
label: "Favorite Color",
|
|
99
|
+
choices: { "r" => "Red", "g" => "Green", "b" => "Blue" },
|
|
100
|
+
appearance: "minimal"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### select
|
|
104
|
+
|
|
105
|
+
Multiple-choice selection:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
select :topics,
|
|
109
|
+
label: "Topics of Interest",
|
|
110
|
+
choices: ["Technology", "Science", "Art", "Music"]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### boolean
|
|
114
|
+
|
|
115
|
+
Yes/no questions:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
boolean :agree, label: "I agree to the terms", appearance: "checkbox"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### range
|
|
122
|
+
|
|
123
|
+
Numeric range slider:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
range :rating,
|
|
127
|
+
label: "Rate 1-10",
|
|
128
|
+
start: 1,
|
|
129
|
+
end: 10,
|
|
130
|
+
step: 1,
|
|
131
|
+
type: :int
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### upload
|
|
135
|
+
|
|
136
|
+
File uploads:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
upload :photo, label: "Take a photo", mediatype: "image/*"
|
|
140
|
+
upload :signature, label: "Signature", mediatype: "image/*", appearance: "signature"
|
|
141
|
+
upload :audio, label: "Record audio", mediatype: "audio/*"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### trigger
|
|
145
|
+
|
|
146
|
+
Action button (acknowledge/confirm):
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
trigger :acknowledge, label: "I have read and understood", appearance: "acknowledge"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### group
|
|
153
|
+
|
|
154
|
+
Logical grouping of fields:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
group :contact_info, label: "Contact Information" do
|
|
158
|
+
input :phone, label: "Phone", type: :string
|
|
159
|
+
input :address, label: "Address", type: :string
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### repeat
|
|
164
|
+
|
|
165
|
+
Repeatable field groups:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
repeat :household_members, label: "Household Members" do
|
|
169
|
+
input :member_name, label: "Name", type: :string
|
|
170
|
+
input :member_age, label: "Age", type: :int
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Form Metadata
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class MyForm < OpenRosa::Form
|
|
178
|
+
form_id "unique_form_id" # Required: unique identifier
|
|
179
|
+
version "1.0.0" # Form version
|
|
180
|
+
name "Human Readable Name" # Display name
|
|
181
|
+
description_text "Form description"
|
|
182
|
+
download_url "https://..." # Optional: auto-generated from base_url if not set
|
|
183
|
+
manifest_url "https://..." # URL for media manifest (if needed)
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Handling Submissions
|
|
188
|
+
|
|
189
|
+
### Global Handler (Middleware)
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
use OpenRosa::Middleware do |config|
|
|
193
|
+
config.forms = [MyForm]
|
|
194
|
+
|
|
195
|
+
config.on_submission do |submission|
|
|
196
|
+
# submission.form_id - Form identifier
|
|
197
|
+
# submission.instance_id - Unique submission ID
|
|
198
|
+
# submission.data - Hash of field values
|
|
199
|
+
# submission.metadata - Submission metadata (timeStart, timeEnd, etc.)
|
|
200
|
+
# submission.raw_xml - Original XML string
|
|
201
|
+
# submission.attachments - Array of uploaded files
|
|
202
|
+
|
|
203
|
+
MyModel.create!(
|
|
204
|
+
form_id: submission.form_id,
|
|
205
|
+
data: submission.data
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
submission.attachments.each do |file|
|
|
209
|
+
# file.filename, file.content_type, file.read, file.size
|
|
210
|
+
save_file(file)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
"Submission received!" # Optional success message
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Per-Form Handler
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
class MyForm < OpenRosa::Form
|
|
222
|
+
form_id "my_form"
|
|
223
|
+
|
|
224
|
+
input :name, label: "Name", type: :string
|
|
225
|
+
|
|
226
|
+
on_submit do |submission|
|
|
227
|
+
# Handle submissions for this specific form
|
|
228
|
+
Record.create!(name: submission.data["name"])
|
|
229
|
+
"Thank you!"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Rails Integration
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# config/application.rb
|
|
238
|
+
module MyApp
|
|
239
|
+
class Application < Rails::Application
|
|
240
|
+
config.middleware.use OpenRosa::Middleware do |config|
|
|
241
|
+
config.forms = [SurveyForm, FeedbackForm]
|
|
242
|
+
config.mount_path = "/openrosa"
|
|
243
|
+
|
|
244
|
+
config.on_submission do |submission|
|
|
245
|
+
FormSubmission.create!(
|
|
246
|
+
form_id: submission.form_id,
|
|
247
|
+
instance_id: submission.instance_id,
|
|
248
|
+
data: submission.data
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Generating XForm XML
|
|
257
|
+
|
|
258
|
+
You can generate the XForm XML directly:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
xml = SurveyForm.to_xml
|
|
262
|
+
# => Complete XForm XML string
|
|
263
|
+
|
|
264
|
+
hash = SurveyForm.form_hash
|
|
265
|
+
# => "md5:abc123..." (for change detection)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Development
|
|
269
|
+
|
|
270
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests and linter.
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
bin/setup
|
|
274
|
+
rake
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Use `bin/console` for an interactive prompt to experiment.
|
|
278
|
+
|
|
279
|
+
## Contributing
|
|
280
|
+
|
|
281
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/alexcwatt/openrosa-ruby.
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Authentication examples for OpenRosa middleware
|
|
4
|
+
|
|
5
|
+
require "open_rosa"
|
|
6
|
+
|
|
7
|
+
# rubocop:disable Metrics/MethodLength
|
|
8
|
+
|
|
9
|
+
# Example 1: HTTP Basic Authentication (most common for ODK clients)
|
|
10
|
+
class BasicAuthExample
|
|
11
|
+
def self.middleware
|
|
12
|
+
OpenRosa::Middleware.new do |config|
|
|
13
|
+
config.forms = [] # Add your forms here
|
|
14
|
+
|
|
15
|
+
config.authenticate do |env|
|
|
16
|
+
auth = Rack::Auth::Basic::Request.new(env)
|
|
17
|
+
if auth.provided? && auth.basic? && auth.credentials
|
|
18
|
+
username, password = auth.credentials
|
|
19
|
+
# Replace with your authentication logic:
|
|
20
|
+
# User.find_by(username: username)&.authenticate(password)
|
|
21
|
+
{ username: username } if username == "admin" && password == "secret"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Optional: Customize the realm shown in browser login prompt
|
|
26
|
+
config.authentication_realm = "ODK Collect"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Example 2: Bearer Token Authentication (e.g. for API clients)
|
|
32
|
+
class TokenAuthExample
|
|
33
|
+
def self.middleware
|
|
34
|
+
OpenRosa::Middleware.new do |config|
|
|
35
|
+
config.forms = [] # Add your forms here
|
|
36
|
+
|
|
37
|
+
config.authenticate do |env|
|
|
38
|
+
token = env["HTTP_AUTHORIZATION"]&.sub(/^Bearer /, "")
|
|
39
|
+
# Replace with your token validation:
|
|
40
|
+
# ApiKey.find_by(token: token, active: true)&.user
|
|
41
|
+
{ token: token } if token == "valid-token-12345"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Example 3: Selective Authentication (public formList, protected submissions)
|
|
48
|
+
class SelectiveAuthExample
|
|
49
|
+
def self.middleware
|
|
50
|
+
OpenRosa::Middleware.new do |config|
|
|
51
|
+
config.forms = [] # Add your forms here
|
|
52
|
+
|
|
53
|
+
config.authenticate do |env|
|
|
54
|
+
auth = Rack::Auth::Basic::Request.new(env)
|
|
55
|
+
if auth.provided? && auth.basic? && auth.credentials
|
|
56
|
+
username, password = auth.credentials
|
|
57
|
+
username == "admin" && password == "secret"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Skip auth for these paths (supports strings and regex)
|
|
62
|
+
config.skip_authentication_for = ["/formList", %r{/forms/}]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Usage in config.ru:
|
|
68
|
+
# require_relative "examples/authentication"
|
|
69
|
+
# run BasicAuthExample.middleware
|
|
70
|
+
#
|
|
71
|
+
# In Rails (config/application.rb):
|
|
72
|
+
# config.middleware.use OpenRosa::Middleware do |config|
|
|
73
|
+
# config.authenticate do |env|
|
|
74
|
+
# # Your auth logic here
|
|
75
|
+
# end
|
|
76
|
+
# end
|
|
77
|
+
#
|
|
78
|
+
# The authenticated user is stored in env["openrosa.authenticated_user"]
|
|
79
|
+
|
|
80
|
+
# rubocop:enable Metrics/MethodLength
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example Rack application demonstrating OpenRosa middleware usage
|
|
4
|
+
#
|
|
5
|
+
# Run this with: rackup examples/rack_app.rb
|
|
6
|
+
# Then access:
|
|
7
|
+
# - http://localhost:9292/openrosa/formList
|
|
8
|
+
# - http://localhost:9292/openrosa/forms/customer_survey
|
|
9
|
+
|
|
10
|
+
require_relative "../lib/open_rosa"
|
|
11
|
+
|
|
12
|
+
# Define a sample customer survey form
|
|
13
|
+
class CustomerSurvey < OpenRosa::Form
|
|
14
|
+
form_id "customer_survey"
|
|
15
|
+
version "1.0.0"
|
|
16
|
+
name "Customer Satisfaction Survey"
|
|
17
|
+
description_text "Please rate your experience with our service"
|
|
18
|
+
download_url "http://localhost:9292/openrosa/forms/customer_survey"
|
|
19
|
+
|
|
20
|
+
# Customer information
|
|
21
|
+
input :customer_name,
|
|
22
|
+
label: "Your Name",
|
|
23
|
+
type: :string,
|
|
24
|
+
required: true
|
|
25
|
+
|
|
26
|
+
input :customer_email,
|
|
27
|
+
label: "Email Address",
|
|
28
|
+
type: :string
|
|
29
|
+
|
|
30
|
+
# Satisfaction rating
|
|
31
|
+
select1 :satisfaction,
|
|
32
|
+
label: "Overall Satisfaction",
|
|
33
|
+
choices: {
|
|
34
|
+
"very_satisfied" => "Very Satisfied",
|
|
35
|
+
"satisfied" => "Satisfied",
|
|
36
|
+
"neutral" => "Neutral",
|
|
37
|
+
"dissatisfied" => "Dissatisfied",
|
|
38
|
+
"very_dissatisfied" => "Very Dissatisfied"
|
|
39
|
+
},
|
|
40
|
+
required: true,
|
|
41
|
+
appearance: "minimal"
|
|
42
|
+
|
|
43
|
+
# Rating scale
|
|
44
|
+
range :service_rating,
|
|
45
|
+
label: "Rate our service (1-10)",
|
|
46
|
+
start: 1,
|
|
47
|
+
end: 10,
|
|
48
|
+
step: 1,
|
|
49
|
+
type: :int
|
|
50
|
+
|
|
51
|
+
# Comments
|
|
52
|
+
input :comments,
|
|
53
|
+
label: "Additional Comments",
|
|
54
|
+
type: :string
|
|
55
|
+
|
|
56
|
+
# Optional photo
|
|
57
|
+
upload :photo,
|
|
58
|
+
label: "Upload a photo (optional)",
|
|
59
|
+
mediatype: "image/*"
|
|
60
|
+
|
|
61
|
+
# Visit date
|
|
62
|
+
input :visit_date,
|
|
63
|
+
label: "Date of Visit",
|
|
64
|
+
type: :date
|
|
65
|
+
|
|
66
|
+
# Would recommend
|
|
67
|
+
boolean :would_recommend,
|
|
68
|
+
label: "Would you recommend us to a friend?",
|
|
69
|
+
appearance: "checkbox"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Define an employee feedback form
|
|
73
|
+
class EmployeeFeedback < OpenRosa::Form
|
|
74
|
+
form_id "employee_feedback"
|
|
75
|
+
version "2.0.0"
|
|
76
|
+
name "Employee Feedback Form"
|
|
77
|
+
description_text "Quarterly employee feedback survey"
|
|
78
|
+
download_url "http://localhost:9292/openrosa/forms/employee_feedback"
|
|
79
|
+
|
|
80
|
+
group :personal_info, label: "Personal Information" do
|
|
81
|
+
input :employee_id,
|
|
82
|
+
label: "Employee ID",
|
|
83
|
+
type: :string,
|
|
84
|
+
required: true
|
|
85
|
+
|
|
86
|
+
input :department,
|
|
87
|
+
label: "Department",
|
|
88
|
+
type: :string
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
group :feedback, label: "Feedback" do
|
|
92
|
+
select :areas_improvement,
|
|
93
|
+
label: "Areas for Improvement",
|
|
94
|
+
choices: [
|
|
95
|
+
"Communication",
|
|
96
|
+
"Work-Life Balance",
|
|
97
|
+
"Career Development",
|
|
98
|
+
"Management",
|
|
99
|
+
"Compensation"
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
input :suggestions,
|
|
103
|
+
label: "Your Suggestions",
|
|
104
|
+
type: :string
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Create a simple Rack app that responds to non-OpenRosa paths
|
|
109
|
+
app = lambda do |env|
|
|
110
|
+
request = Rack::Request.new(env)
|
|
111
|
+
|
|
112
|
+
if request.path == "/"
|
|
113
|
+
[
|
|
114
|
+
200,
|
|
115
|
+
{ "Content-Type" => "text/html" },
|
|
116
|
+
[<<~HTML]
|
|
117
|
+
<!DOCTYPE html>
|
|
118
|
+
<html>
|
|
119
|
+
<head><title>OpenRosa Example Server</title></head>
|
|
120
|
+
<body>
|
|
121
|
+
<h1>OpenRosa Example Server</h1>
|
|
122
|
+
<h2>Available Endpoints:</h2>
|
|
123
|
+
<ul>
|
|
124
|
+
<li><a href="/openrosa/formList">GET /openrosa/formList</a> - List all forms</li>
|
|
125
|
+
<li><a href="/openrosa/formList?formID=customer_survey">GET /openrosa/formList?formID=customer_survey</a> - Filter by form ID</li>
|
|
126
|
+
<li><a href="/openrosa/formList?verbose=true">GET /openrosa/formList?verbose=true</a> - Verbose mode</li>
|
|
127
|
+
<li><a href="/openrosa/forms/customer_survey">GET /openrosa/forms/customer_survey</a> - Download Customer Survey form</li>
|
|
128
|
+
<li><a href="/openrosa/forms/employee_feedback">GET /openrosa/forms/employee_feedback</a> - Download Employee Feedback form</li>
|
|
129
|
+
<li>HEAD /openrosa/submission - Pre-flight check for submissions</li>
|
|
130
|
+
<li>POST /openrosa/submission - Submit form data (multipart/form-data)</li>
|
|
131
|
+
</ul>
|
|
132
|
+
<h2>Available Forms:</h2>
|
|
133
|
+
<ul>
|
|
134
|
+
<li><strong>customer_survey</strong> - Customer Satisfaction Survey (v1.0.0)</li>
|
|
135
|
+
<li><strong>employee_feedback</strong> - Employee Feedback Form (v2.0.0)</li>
|
|
136
|
+
</ul>
|
|
137
|
+
</body>
|
|
138
|
+
</html>
|
|
139
|
+
HTML
|
|
140
|
+
]
|
|
141
|
+
else
|
|
142
|
+
[404, { "Content-Type" => "text/plain" }, ["Not Found"]]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Wrap the app with OpenRosa middleware
|
|
147
|
+
use OpenRosa::Middleware do |config|
|
|
148
|
+
config.forms = [CustomerSurvey, EmployeeFeedback]
|
|
149
|
+
config.mount_path = "/openrosa"
|
|
150
|
+
|
|
151
|
+
# Handle form submissions
|
|
152
|
+
config.on_submission do |submission|
|
|
153
|
+
# Print submission details to console
|
|
154
|
+
puts "\n=== Form Submission Received ==="
|
|
155
|
+
puts "Form ID: #{submission.form_id}"
|
|
156
|
+
puts "Instance ID: #{submission.instance_id}"
|
|
157
|
+
puts "Submitted at: #{submission.metadata[:timeEnd]}"
|
|
158
|
+
puts "Data:"
|
|
159
|
+
submission.data.each do |key, value|
|
|
160
|
+
puts " #{key}: #{value}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if submission.attachments.any?
|
|
164
|
+
puts "Attachments:"
|
|
165
|
+
submission.attachments.each do |att|
|
|
166
|
+
puts " - #{att.filename} (#{att.content_type}, #{att.size} bytes)"
|
|
167
|
+
# In a real app, you'd save these files:
|
|
168
|
+
# File.write("uploads/#{att.filename}", att.read)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
puts "================================\n"
|
|
172
|
+
|
|
173
|
+
# Return a custom success message
|
|
174
|
+
"Thank you! Your #{submission.form_id} submission has been received."
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
run app
|