standard_procedure_documents 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/README.md +109 -0
- data/Rakefile +16 -0
- data/app/models/concerns/documents/container.rb +44 -0
- data/app/models/documents/checkbox_value.rb +15 -0
- data/app/models/documents/data_value.rb +12 -0
- data/app/models/documents/date_value.rb +13 -0
- data/app/models/documents/decimal_value.rb +14 -0
- data/app/models/documents/download.rb +9 -0
- data/app/models/documents/element.rb +19 -0
- data/app/models/documents/email_value.rb +5 -0
- data/app/models/documents/field_value.rb +17 -0
- data/app/models/documents/file_value.rb +14 -0
- data/app/models/documents/form.rb +12 -0
- data/app/models/documents/form_section.rb +9 -0
- data/app/models/documents/image.rb +14 -0
- data/app/models/documents/image_value.rb +9 -0
- data/app/models/documents/location_value.rb +12 -0
- data/app/models/documents/multi_select_value.rb +32 -0
- data/app/models/documents/number_value.rb +12 -0
- data/app/models/documents/page_break.rb +4 -0
- data/app/models/documents/paragraph.rb +6 -0
- data/app/models/documents/phone_value.rb +5 -0
- data/app/models/documents/rich_text_value.rb +5 -0
- data/app/models/documents/select_value.rb +20 -0
- data/app/models/documents/signature_value.rb +7 -0
- data/app/models/documents/table.rb +6 -0
- data/app/models/documents/text_value.rb +11 -0
- data/app/models/documents/time_value.rb +13 -0
- data/app/models/documents/url_value.rb +5 -0
- data/app/models/documents/video.rb +6 -0
- data/config/locales/en.yml +43 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20250721133645_create_documents_elements.rb +19 -0
- data/db/migrate/20250721135716_create_documents_form_sections.rb +10 -0
- data/db/migrate/20250721135938_create_documents_field_values.rb +18 -0
- data/lib/documents/document_definition.rb +16 -0
- data/lib/documents/element_definition.rb +27 -0
- data/lib/documents/engine.rb +11 -0
- data/lib/documents/field_definition.rb +22 -0
- data/lib/documents/version.rb +3 -0
- data/lib/documents.rb +11 -0
- data/lib/standard_procedure_documents.rb +1 -0
- data/lib/tasks/documents_tasks.rake +4 -0
- metadata +143 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: decc31ab7e7563240ca1579b908f8cb248c9f27944bb7c6299e14c90b5cbcd38
|
4
|
+
data.tar.gz: 3211e2a568e212200b65f38c184043854f314c095abd448f361436562fde4423
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 48ce632d104a90b03672f1d221936ff1e3c924070da7fd8fc5178e2297fc526af05ed1bf5fff2663cc942196bc7e694dcf72984a7d17baaa19556cc796ef0ebf
|
7
|
+
data.tar.gz: fdc1f364dcd3f00ac0484a75d6c5e376c8885aa9a9679c57c5d3eaebdec30267f04510a462d012a17a61e7b25a6016f4780fbdd193803ba670ae0d498ba1f747
|
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# Documents
|
2
|
+
Configurable documents
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
|
6
|
+
### Creating a document from a configuration file
|
7
|
+
|
8
|
+
Build a document using a JSON or YAML file.
|
9
|
+
|
10
|
+
This uses [dry-validation](https://dry-rb.org/gems/dry-validation/1.10/) to ensure the schema is valid.
|
11
|
+
|
12
|
+
```yaml
|
13
|
+
title: Order Form
|
14
|
+
elements:
|
15
|
+
- element: paragraph
|
16
|
+
html: <h1>Order Form</h1>
|
17
|
+
- element: paragraph
|
18
|
+
html: <p>Place details of your order below
|
19
|
+
- element: form
|
20
|
+
section_type: static
|
21
|
+
display_type: form
|
22
|
+
fields:
|
23
|
+
- name: company
|
24
|
+
description: Company
|
25
|
+
field_type: "Documents::TextValue"
|
26
|
+
required: true
|
27
|
+
- name: order_date
|
28
|
+
description: Date
|
29
|
+
field_type: "Documents::DateValue"
|
30
|
+
required: true
|
31
|
+
default_value: Date.now
|
32
|
+
- element: form
|
33
|
+
section_type: repeating
|
34
|
+
display_type: table
|
35
|
+
fields:
|
36
|
+
- name: item
|
37
|
+
description: Item
|
38
|
+
field_type: "Documents::TextValue"
|
39
|
+
required: true
|
40
|
+
- name: quantity
|
41
|
+
description: Quantity
|
42
|
+
field_type: "Documents::IntegerValue"
|
43
|
+
required: true
|
44
|
+
default_value: 1
|
45
|
+
- element: form
|
46
|
+
section_type: static
|
47
|
+
display_type: form
|
48
|
+
fields:
|
49
|
+
- name: ordered_by
|
50
|
+
description: Your name
|
51
|
+
field_type: "Documents::TextValue"
|
52
|
+
required: true
|
53
|
+
- name: signature
|
54
|
+
description: Signed
|
55
|
+
field_type: "Documents::SignatureValue"
|
56
|
+
required: true
|
57
|
+
```
|
58
|
+
|
59
|
+
Create a "container" - a record within your application that will hold this order form. Then use the `Document::ElementBuilder` to add it:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
@order_form = OrderForm.create!
|
63
|
+
expect(@order_form).to be_kind_of(Documents::Container)
|
64
|
+
|
65
|
+
@configuration = YAML.load(File.read("order_form.yml"))
|
66
|
+
@order_form.load_elements_from(@configuration)
|
67
|
+
|
68
|
+
expect(@order_form.elements.first).to be_kind_of(Documents::Paragraph)
|
69
|
+
expect(@order_form.elements.last).to be_kind_of(Documents::Form)
|
70
|
+
expect(@order_form.elements.last.fields.last).to be_kind_of(Documents::SignatureValue)
|
71
|
+
```
|
72
|
+
|
73
|
+
### Document Elements
|
74
|
+
|
75
|
+
Documents are built out of an ordered list of Elements.
|
76
|
+
|
77
|
+
Elements can be content, such as Paragraphs, Images, Tables and Forms (although currently only Paragraphs and Forms can be loaded from a configuration file).
|
78
|
+
|
79
|
+
### Configuring Forms
|
80
|
+
|
81
|
+
Forms are split into two types - static and repeating - and repeating forms can be displayed as a form or a table.
|
82
|
+
|
83
|
+
A static form consists of an ordered list of FieldValues, each with a type, such as `TextValue`, `NumberValue`, `SelectValue` and so on. A repeating form also has an ordered list of FieldValues but the end-user can choose to repeat that group multiple times - for example, in an order form, you may wish to place multiple items on a single form.
|
84
|
+
|
85
|
+
FieldValues can be marked as `required`, `allow_comments` (so the end-user can add arbitrary text to their answer), `allow_attachments` (so the end-user can upload and attach files, photos and other documents to support their answer). They can also specify a `default_value` (the meaning of which varies according to the field type) and select and multi-select values also have `options` - key/value pairs that they can pick in the user-interface.
|
86
|
+
|
87
|
+
Finally, FieldValues can also be marked as `allow_tasks` - which means that a follow-up task system can be used alongside the form itself (for example, if performing a safety inspection, noting something that is not compliant and therefore assigning a fix to another person).
|
88
|
+
|
89
|
+
## Installation
|
90
|
+
Add this line to your application's Gemfile:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
gem "standard_procedure_documents"
|
94
|
+
```
|
95
|
+
|
96
|
+
And then execute:
|
97
|
+
```bash
|
98
|
+
$ bundle
|
99
|
+
```
|
100
|
+
Then copy the migrations to your Rails application:
|
101
|
+
```bash
|
102
|
+
bin/rails standard_procedure_documents:migrations:install db:migrate db:test:prepare
|
103
|
+
```
|
104
|
+
|
105
|
+
## Contributing
|
106
|
+
Contributions welcome
|
107
|
+
|
108
|
+
## License
|
109
|
+
The gem is available as open source under the terms of the [LGPL License](/LICENCE).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
APP_RAKEFILE = File.expand_path("spec/test_app/Rakefile", __dir__)
|
4
|
+
load "rails/tasks/engine.rake"
|
5
|
+
|
6
|
+
load "rails/tasks/statistics.rake"
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
|
10
|
+
require "rspec/core"
|
11
|
+
require "rspec/core/rake_task"
|
12
|
+
|
13
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
14
|
+
RSpec::Core::RakeTask.new(spec: "app:db:test:prepare")
|
15
|
+
|
16
|
+
task default: :spec
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Documents::Container
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
has_many :elements, -> { order :position }, class_name: "Documents::Element", as: :container, dependent: :destroy
|
6
|
+
end
|
7
|
+
|
8
|
+
def load_elements_from configuration
|
9
|
+
result = Documents::DocumentDefinition.new.call(configuration)
|
10
|
+
raise ArgumentError.new(result.errors.to_h.to_json) if result.errors.any?
|
11
|
+
|
12
|
+
configuration["elements"].each do |element_config|
|
13
|
+
case element_config["element"]
|
14
|
+
when "paragraph"
|
15
|
+
elements.create!(
|
16
|
+
type: "Documents::Paragraph",
|
17
|
+
position: :last,
|
18
|
+
html: element_config["html"],
|
19
|
+
description: element_config["description"] || ""
|
20
|
+
)
|
21
|
+
when "form"
|
22
|
+
form = elements.create!(
|
23
|
+
type: "Documents::Form",
|
24
|
+
position: :last,
|
25
|
+
section_type: element_config["section_type"],
|
26
|
+
display_type: element_config["display_type"],
|
27
|
+
description: element_config["description"] || ""
|
28
|
+
)
|
29
|
+
|
30
|
+
# Create field values for the first section
|
31
|
+
section = form.sections.first
|
32
|
+
element_config["fields"]&.each do |field_config|
|
33
|
+
section.field_values.create!(
|
34
|
+
type: field_config["field_type"],
|
35
|
+
name: field_config["name"],
|
36
|
+
description: field_config["description"],
|
37
|
+
required: field_config["required"] || false,
|
38
|
+
position: :last
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Documents
|
2
|
+
class CheckboxValue < FieldValue
|
3
|
+
has_attribute :value, :boolean
|
4
|
+
before_validation :set_default_value, if: -> { value.nil? && default_value.present? }
|
5
|
+
validates :value, inclusion: {in: [true, false], message: :invalid_boolean}, on: :update, if: -> { required? }
|
6
|
+
|
7
|
+
def to_s = value ? "☑️" : "⊗"
|
8
|
+
|
9
|
+
private def set_default_value
|
10
|
+
self.value = DEFAULTS[default_value.to_s].nil? ? ActiveModel::Type::Boolean.new.cast(default_value) : DEFAULTS[default_value.to_s]
|
11
|
+
end
|
12
|
+
|
13
|
+
DEFAULTS = {"yes" => true, "y" => true, "no" => false, "n" => false}.freeze
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Documents
|
2
|
+
class DataValue < FieldValue
|
3
|
+
has_model :value
|
4
|
+
has_attribute :data_class, :string, default: ""
|
5
|
+
validates :value, presence: true, on: :update, if: -> { required? }
|
6
|
+
validate :value_is_correct_class, if: -> { data_class.present? }
|
7
|
+
|
8
|
+
private def value_is_correct_class
|
9
|
+
errors.add :value, :invalid_data_class if value.present? && !value.is_a?(data_class.constantize)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Documents
|
2
|
+
class DateValue < FieldValue
|
3
|
+
has_attribute :value, :date
|
4
|
+
before_validation :set_default_value, if: -> { value.blank? && default_value.present? }
|
5
|
+
validates :value, presence: true, on: :update, if: -> { required? }
|
6
|
+
|
7
|
+
def to_s = value.present? ? I18n.l(value, format: :long) : ""
|
8
|
+
|
9
|
+
private def set_default_value
|
10
|
+
self.value = (default_value == "today") ? Date.current : Date.parse(default_value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Documents
|
2
|
+
class DecimalValue < FieldValue
|
3
|
+
has_attribute :value, :decimal
|
4
|
+
before_validation :set_default_value, if: -> { value.blank? && default_value.present? }
|
5
|
+
validates :value, presence: true, on: :update, if: -> { required? }
|
6
|
+
validates :value, numericality: true, allow_blank: true
|
7
|
+
|
8
|
+
def to_s = value.present? ? value.to_f.round(2) : ""
|
9
|
+
|
10
|
+
private def set_default_value
|
11
|
+
self.value = default_value.to_f
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Documents
|
2
|
+
class Element < ApplicationRecord
|
3
|
+
include HasAttributes
|
4
|
+
serialize :data, type: Hash, coder: JSON
|
5
|
+
belongs_to :container, polymorphic: true
|
6
|
+
validate :container_is_legal
|
7
|
+
positioned on: :container
|
8
|
+
attribute :description, :string, default: ""
|
9
|
+
has_one_attached :file
|
10
|
+
|
11
|
+
def copy_to(target_container, copy_as_template: false) = nil
|
12
|
+
|
13
|
+
def path = position.to_s
|
14
|
+
|
15
|
+
private def container_is_legal
|
16
|
+
errors.add :container, :invalid unless container.is_a? Documents::Container
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Documents
|
2
|
+
class FieldValue < ApplicationRecord
|
3
|
+
include HasAttributes
|
4
|
+
serialize :data, type: Hash, coder: JSON
|
5
|
+
belongs_to :section, class_name: "FormSection"
|
6
|
+
positioned on: :section
|
7
|
+
has_attribute :default_value, :string
|
8
|
+
has_many_attached :files
|
9
|
+
has_many_attached :attachments
|
10
|
+
has_attribute :comments, :string, default: ""
|
11
|
+
def has_value? = value.present?
|
12
|
+
|
13
|
+
def path = [section.path, name.to_s].join("/")
|
14
|
+
|
15
|
+
def to_s = value.to_s
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Documents
|
2
|
+
class FileValue < FieldValue
|
3
|
+
def value = files
|
4
|
+
validate :files_are_attached, on: :update, if: -> { required? }
|
5
|
+
|
6
|
+
def has_value? = files.attached?
|
7
|
+
|
8
|
+
def to_s = files.map(&:filename).map(&:to_s).join(", ")
|
9
|
+
|
10
|
+
private def files_are_attached
|
11
|
+
errors.add :files, :blank unless files.attached?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Documents
|
2
|
+
class Form < Element
|
3
|
+
enum :section_type, {static: 0, repeating: 1}, prefix: true
|
4
|
+
enum :display_type, {form: 0, table: 1}, prefix: true
|
5
|
+
enum :form_submission_status, draft: 0, submitted: 1, cancelled: -1
|
6
|
+
|
7
|
+
has_many :sections, -> { order :position }, class_name: "FormSection", dependent: :destroy
|
8
|
+
after_save :create_first_section, if: -> { sections.empty? }
|
9
|
+
|
10
|
+
private def create_first_section = sections.create!
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Documents
|
2
|
+
class FormSection < ApplicationRecord
|
3
|
+
belongs_to :form, inverse_of: :sections
|
4
|
+
positioned on: :form
|
5
|
+
has_many :field_values, -> { order :position }, inverse_of: :section, dependent: :destroy
|
6
|
+
|
7
|
+
def path = [form.path, position.to_s].join("/")
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Documents
|
2
|
+
class Image < Element
|
3
|
+
validate :file_is_attached
|
4
|
+
validate :file_is_image
|
5
|
+
|
6
|
+
private def file_is_attached
|
7
|
+
errors.add :file, :blank unless file.attached?
|
8
|
+
end
|
9
|
+
|
10
|
+
private def file_is_image
|
11
|
+
errors.add :file, :invalid unless file.image?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Documents
|
2
|
+
class LocationValue < FieldValue
|
3
|
+
has_attribute :latitude, :float
|
4
|
+
validates :latitude, presence: true, on: :update, if: -> { required? }
|
5
|
+
has_attribute :longitude, :float
|
6
|
+
validates :longitude, presence: true, on: :update, if: -> { required? }
|
7
|
+
|
8
|
+
def value = {longitude: longitude, latitude: latitude}
|
9
|
+
|
10
|
+
def to_s = value.to_json
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Documents
|
2
|
+
class MultiSelectValue < FieldValue
|
3
|
+
has_attribute :value, :json, default: []
|
4
|
+
has_attribute :options, :json, default: {}
|
5
|
+
before_validation :set_default_value, if: -> { value.blank? && default_value.present? }
|
6
|
+
validates :value, presence: {message: :required}, on: :update, if: -> { required? }
|
7
|
+
validate :all_values_are_valid_options, if: -> { value.present? && options.any? }
|
8
|
+
validate :default_value_contains_valid_options, if: -> { default_value.present? && options.any? }
|
9
|
+
|
10
|
+
def to_s = Array.wrap(value).map { |key| options[key] || key }.join(", ")
|
11
|
+
|
12
|
+
private def set_default_value
|
13
|
+
self.value = Array.wrap(parse_default_value)
|
14
|
+
end
|
15
|
+
|
16
|
+
private def all_values_are_valid_options
|
17
|
+
errors.add :value, :invalid_option if invalid_keys_in?(value)
|
18
|
+
end
|
19
|
+
|
20
|
+
private def default_value_contains_valid_options
|
21
|
+
errors.add :default_value, :invalid_default_option if invalid_keys_in?(parse_default_value)
|
22
|
+
end
|
23
|
+
|
24
|
+
private def invalid_keys_in?(keys) = (Array.wrap(keys) - options.keys).any?
|
25
|
+
|
26
|
+
private def parse_default_value
|
27
|
+
JSON.parse(default_value.to_s)
|
28
|
+
rescue JSON::ParserError
|
29
|
+
[default_value]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Documents
|
2
|
+
class NumberValue < FieldValue
|
3
|
+
has_attribute :value, :integer
|
4
|
+
before_validation :set_default_value, if: -> { value.blank? && default_value.present? }
|
5
|
+
validates :value, presence: true, on: :update, if: -> { required? }
|
6
|
+
validates :value, numericality: {only_integer: true}, allow_blank: true
|
7
|
+
|
8
|
+
private def set_default_value
|
9
|
+
self.value = default_value.to_i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Documents
|
2
|
+
class SelectValue < FieldValue
|
3
|
+
has_attribute :value, :string
|
4
|
+
has_attribute :options, :json, default: {}
|
5
|
+
before_validation :set_default_value, if: -> { value.blank? && default_value.present? }
|
6
|
+
validates :value, presence: {message: :required}, on: :update, if: -> { required? }
|
7
|
+
validates :value, inclusion: {in: ->(record) { record.options.keys }, message: :invalid_option}, on: :update, allow_blank: true
|
8
|
+
validate :default_value_is_valid_option, if: -> { default_value.present? && options.any? }
|
9
|
+
|
10
|
+
def to_s = value.present? ? (options[value] || value) : ""
|
11
|
+
|
12
|
+
private def set_default_value
|
13
|
+
self.value = default_value
|
14
|
+
end
|
15
|
+
|
16
|
+
private def default_value_is_valid_option
|
17
|
+
errors.add :default_value, :invalid_default_option unless options.key?(default_value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
module Documents
|
2
|
+
class SignatureValue < FieldValue
|
3
|
+
has_attribute :value, :text
|
4
|
+
validates :value, presence: {message: :required_signature}, on: :update, if: -> { required? }
|
5
|
+
validates :value, format: {with: /\Adata:image\/(png|svg\+xml);base64,[A-Za-z0-9+\/]+=*\z/, message: :invalid_signature}, on: :update, allow_blank: true
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Documents
|
2
|
+
class TextValue < FieldValue
|
3
|
+
has_attribute :value, :string
|
4
|
+
before_validation :set_default_value, if: -> { value.blank? && default_value.present? }
|
5
|
+
validates :value, presence: true, on: :update, if: -> { required? }
|
6
|
+
|
7
|
+
private def set_default_value
|
8
|
+
self.value = default_value
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Documents
|
2
|
+
class TimeValue < FieldValue
|
3
|
+
has_attribute :value, :datetime
|
4
|
+
before_validation :set_default_value, if: -> { value.blank? && default_value.present? }
|
5
|
+
validates :value, presence: true, on: :update, if: -> { required? }
|
6
|
+
|
7
|
+
def to_s = value.present? ? I18n.l(value, format: :short) : ""
|
8
|
+
|
9
|
+
private def set_default_value
|
10
|
+
self.value = (default_value == "now") ? Time.current : Time.parse(default_value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
en:
|
2
|
+
activerecord:
|
3
|
+
errors:
|
4
|
+
models:
|
5
|
+
documents/checkbox_value:
|
6
|
+
attributes:
|
7
|
+
value:
|
8
|
+
invalid_boolean: must be true or false
|
9
|
+
documents/data_value:
|
10
|
+
attributes:
|
11
|
+
value:
|
12
|
+
invalid_data_class: Invalid data class
|
13
|
+
documents/email_value:
|
14
|
+
attributes:
|
15
|
+
value:
|
16
|
+
invalid_email: must be a valid email address
|
17
|
+
documents/multi_select_value:
|
18
|
+
attributes:
|
19
|
+
default_value:
|
20
|
+
invalid_default_option: Invalid default option
|
21
|
+
value:
|
22
|
+
invalid_option: Invalid option
|
23
|
+
required: Required
|
24
|
+
documents/phone_value:
|
25
|
+
attributes:
|
26
|
+
value:
|
27
|
+
invalid_phone: must be a valid phone number
|
28
|
+
documents/select_value:
|
29
|
+
attributes:
|
30
|
+
default_value:
|
31
|
+
invalid_default_option: Invalid default option
|
32
|
+
value:
|
33
|
+
invalid_option: Invalid option
|
34
|
+
required: Required
|
35
|
+
documents/signature_value:
|
36
|
+
attributes:
|
37
|
+
value:
|
38
|
+
invalid_signature: Invalid signature
|
39
|
+
required_signature: Required signature
|
40
|
+
documents/url_value:
|
41
|
+
attributes:
|
42
|
+
value:
|
43
|
+
invalid_url: must be a valid URL
|
data/config/routes.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateDocumentsElements < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :documents_elements do |t|
|
4
|
+
t.string :type
|
5
|
+
t.belongs_to :container, polymorphic: true, index: true
|
6
|
+
t.integer :position, null: false
|
7
|
+
t.text :description
|
8
|
+
t.text :html
|
9
|
+
t.string :url
|
10
|
+
t.integer :columns, default: 0, null: false
|
11
|
+
t.integer :section_type, default: 0, null: false
|
12
|
+
t.integer :display_type, default: 0, null: false
|
13
|
+
t.integer :form_submission_status, default: 0, null: false
|
14
|
+
t.text :data
|
15
|
+
t.timestamps
|
16
|
+
t.index [:container_type, :container_id, :position], unique: true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class CreateDocumentsFormSections < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :documents_form_sections do |t|
|
4
|
+
t.belongs_to :form, foreign_key: {to_table: "documents_elements"}
|
5
|
+
t.integer :position, null: false
|
6
|
+
t.timestamps
|
7
|
+
t.index [:form_id, :position], unique: true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateDocumentsFieldValues < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :documents_field_values do |t|
|
4
|
+
t.belongs_to :section, foreign_key: {to_table: "documents_form_sections"}
|
5
|
+
t.integer :position, null: false
|
6
|
+
t.text :data
|
7
|
+
t.string :name, null: false
|
8
|
+
t.string :description, null: false
|
9
|
+
t.string :type
|
10
|
+
t.boolean :required, default: false, null: false
|
11
|
+
t.boolean :allow_comments, default: false, null: false
|
12
|
+
t.boolean :allow_attachments, default: false, null: false
|
13
|
+
t.boolean :allow_tasks, default: false, null: false
|
14
|
+
t.timestamps
|
15
|
+
t.index [:section_id, :position], unique: true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Documents
|
2
|
+
# Describes a document template, built out of multiple "elements".
|
3
|
+
# Each element can be a paragraph or other piece of content,
|
4
|
+
# or it could be the definition of a "form",
|
5
|
+
# describing the questions the eventual document will contain
|
6
|
+
# that must be answered by the end-user
|
7
|
+
|
8
|
+
DocumentDefinitionSchema = Dry::Schema.Params do
|
9
|
+
required(:title).filled(:string)
|
10
|
+
required(:elements).array(Documents::ElementDefinitionSchema)
|
11
|
+
end
|
12
|
+
|
13
|
+
class DocumentDefinition < Dry::Validation::Contract
|
14
|
+
params(DocumentDefinitionSchema)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Documents
|
2
|
+
ElementDefinitionSchema = Dry::Schema.Params do
|
3
|
+
required(:element).filled(:string, included_in?: %w[paragraph form])
|
4
|
+
optional(:description).maybe(:string)
|
5
|
+
optional(:html).filled(:string)
|
6
|
+
optional(:section_type).filled(:string, included_in?: %w[static repeating])
|
7
|
+
optional(:display_type).filled(:string, included_in?: %w[form table])
|
8
|
+
optional(:fields).array(Documents::FieldDefinitionSchema)
|
9
|
+
end
|
10
|
+
|
11
|
+
class ElementDefinition < Dry::Validation::Contract
|
12
|
+
params(ElementDefinitionSchema)
|
13
|
+
|
14
|
+
rule :html do
|
15
|
+
key.failure(:blank) if (values[:element] == "paragraph") && values[:html].blank?
|
16
|
+
end
|
17
|
+
rule :section_type do
|
18
|
+
key.failure(:blank) if (values[:element] == "form") && values[:section_type].blank?
|
19
|
+
end
|
20
|
+
rule :display_type do
|
21
|
+
key.failure(:blank) if (values[:element] == "form") && values[:display_type].blank?
|
22
|
+
end
|
23
|
+
rule :fields do
|
24
|
+
key.failure(:blank) if (values[:element] == "form") && values[:fields].empty?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Documents
|
2
|
+
FieldDefinitionSchema = Dry::Schema.Params do
|
3
|
+
required(:name).filled(:string)
|
4
|
+
required(:description).filled(:string)
|
5
|
+
required(:field_type).filled(included_in?: %w[Documents::CheckboxValue Documents::DateValue Documents::DateTimeValue Documents::DecimalValue Documents::EmailValue Documents::FileValue Documents::LocationValue Documents::ImageValue Documents::DataValue Documents::MultiSelectValue Documents::NumberValue Documents::PhoneValue Documents::RichTextValue Documents::SelectValue Documents::TextValue Documents::TimeValue Documents::UrlValue Documents::SignatureValue])
|
6
|
+
required(:required).filled(:bool)
|
7
|
+
optional(:allow_comments).filled(:bool)
|
8
|
+
optional(:allow_attachments).filled(:bool)
|
9
|
+
optional(:allow_tasks).filled(:bool)
|
10
|
+
optional(:default_value).maybe(:string)
|
11
|
+
optional(:options).filled(:hash)
|
12
|
+
optional(:data_class).filled(:string)
|
13
|
+
end
|
14
|
+
|
15
|
+
class FieldDefinition < Dry::Validation::Contract
|
16
|
+
params(FieldDefinitionSchema)
|
17
|
+
|
18
|
+
rule(:options) do
|
19
|
+
key.failure(:blank) if %w[Documents::SelectValue Documents::MultiSelectValue].include?(values[:field_type]) && values[:options].empty?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/documents.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "dry/validation"
|
2
|
+
require "positioning"
|
3
|
+
require "has_attributes"
|
4
|
+
require "documents/version"
|
5
|
+
require "documents/engine"
|
6
|
+
|
7
|
+
module Documents
|
8
|
+
require_relative "documents/field_definition"
|
9
|
+
require_relative "documents/element_definition"
|
10
|
+
require_relative "documents/document_definition"
|
11
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "documents"
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: standard_procedure_documents
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rahoul Baruah
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-07-21 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 7.1.3
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 7.1.3
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: dry-validation
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: standard_procedure_has_attributes
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: positioning
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
description: Documents
|
69
|
+
email:
|
70
|
+
- rahoulb@echodek.co
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- app/models/concerns/documents/container.rb
|
78
|
+
- app/models/documents/checkbox_value.rb
|
79
|
+
- app/models/documents/data_value.rb
|
80
|
+
- app/models/documents/date_value.rb
|
81
|
+
- app/models/documents/decimal_value.rb
|
82
|
+
- app/models/documents/download.rb
|
83
|
+
- app/models/documents/element.rb
|
84
|
+
- app/models/documents/email_value.rb
|
85
|
+
- app/models/documents/field_value.rb
|
86
|
+
- app/models/documents/file_value.rb
|
87
|
+
- app/models/documents/form.rb
|
88
|
+
- app/models/documents/form_section.rb
|
89
|
+
- app/models/documents/image.rb
|
90
|
+
- app/models/documents/image_value.rb
|
91
|
+
- app/models/documents/location_value.rb
|
92
|
+
- app/models/documents/multi_select_value.rb
|
93
|
+
- app/models/documents/number_value.rb
|
94
|
+
- app/models/documents/page_break.rb
|
95
|
+
- app/models/documents/paragraph.rb
|
96
|
+
- app/models/documents/phone_value.rb
|
97
|
+
- app/models/documents/rich_text_value.rb
|
98
|
+
- app/models/documents/select_value.rb
|
99
|
+
- app/models/documents/signature_value.rb
|
100
|
+
- app/models/documents/table.rb
|
101
|
+
- app/models/documents/text_value.rb
|
102
|
+
- app/models/documents/time_value.rb
|
103
|
+
- app/models/documents/url_value.rb
|
104
|
+
- app/models/documents/video.rb
|
105
|
+
- config/locales/en.yml
|
106
|
+
- config/routes.rb
|
107
|
+
- db/migrate/20250721133645_create_documents_elements.rb
|
108
|
+
- db/migrate/20250721135716_create_documents_form_sections.rb
|
109
|
+
- db/migrate/20250721135938_create_documents_field_values.rb
|
110
|
+
- lib/documents.rb
|
111
|
+
- lib/documents/document_definition.rb
|
112
|
+
- lib/documents/element_definition.rb
|
113
|
+
- lib/documents/engine.rb
|
114
|
+
- lib/documents/field_definition.rb
|
115
|
+
- lib/documents/version.rb
|
116
|
+
- lib/standard_procedure_documents.rb
|
117
|
+
- lib/tasks/documents_tasks.rake
|
118
|
+
homepage: https://theartandscienceofruby.com/
|
119
|
+
licenses:
|
120
|
+
- LGPL
|
121
|
+
metadata:
|
122
|
+
allowed_push_host: https://rubygems.org
|
123
|
+
homepage_uri: https://theartandscienceofruby.com/
|
124
|
+
source_code_uri: https://github.com/standard_procedure
|
125
|
+
changelog_uri: https://github.com/standard_procedure
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubygems_version: 3.6.2
|
141
|
+
specification_version: 4
|
142
|
+
summary: 'Standard Procedure: Documents'
|
143
|
+
test_files: []
|