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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +109 -0
  3. data/Rakefile +16 -0
  4. data/app/models/concerns/documents/container.rb +44 -0
  5. data/app/models/documents/checkbox_value.rb +15 -0
  6. data/app/models/documents/data_value.rb +12 -0
  7. data/app/models/documents/date_value.rb +13 -0
  8. data/app/models/documents/decimal_value.rb +14 -0
  9. data/app/models/documents/download.rb +9 -0
  10. data/app/models/documents/element.rb +19 -0
  11. data/app/models/documents/email_value.rb +5 -0
  12. data/app/models/documents/field_value.rb +17 -0
  13. data/app/models/documents/file_value.rb +14 -0
  14. data/app/models/documents/form.rb +12 -0
  15. data/app/models/documents/form_section.rb +9 -0
  16. data/app/models/documents/image.rb +14 -0
  17. data/app/models/documents/image_value.rb +9 -0
  18. data/app/models/documents/location_value.rb +12 -0
  19. data/app/models/documents/multi_select_value.rb +32 -0
  20. data/app/models/documents/number_value.rb +12 -0
  21. data/app/models/documents/page_break.rb +4 -0
  22. data/app/models/documents/paragraph.rb +6 -0
  23. data/app/models/documents/phone_value.rb +5 -0
  24. data/app/models/documents/rich_text_value.rb +5 -0
  25. data/app/models/documents/select_value.rb +20 -0
  26. data/app/models/documents/signature_value.rb +7 -0
  27. data/app/models/documents/table.rb +6 -0
  28. data/app/models/documents/text_value.rb +11 -0
  29. data/app/models/documents/time_value.rb +13 -0
  30. data/app/models/documents/url_value.rb +5 -0
  31. data/app/models/documents/video.rb +6 -0
  32. data/config/locales/en.yml +43 -0
  33. data/config/routes.rb +2 -0
  34. data/db/migrate/20250721133645_create_documents_elements.rb +19 -0
  35. data/db/migrate/20250721135716_create_documents_form_sections.rb +10 -0
  36. data/db/migrate/20250721135938_create_documents_field_values.rb +18 -0
  37. data/lib/documents/document_definition.rb +16 -0
  38. data/lib/documents/element_definition.rb +27 -0
  39. data/lib/documents/engine.rb +11 -0
  40. data/lib/documents/field_definition.rb +22 -0
  41. data/lib/documents/version.rb +3 -0
  42. data/lib/documents.rb +11 -0
  43. data/lib/standard_procedure_documents.rb +1 -0
  44. data/lib/tasks/documents_tasks.rake +4 -0
  45. 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,9 @@
1
+ module Documents
2
+ class Download < Element
3
+ validate :file_is_attached
4
+
5
+ private def file_is_attached
6
+ errors.add :file, :blank unless file.attached?
7
+ end
8
+ end
9
+ 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,5 @@
1
+ module Documents
2
+ class EmailValue < TextValue
3
+ validates :value, format: {with: URI::MailTo::EMAIL_REGEXP, message: :invalid_email}, on: :update, allow_blank: true
4
+ end
5
+ 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,9 @@
1
+ module Documents
2
+ class ImageValue < FileValue
3
+ validate :files_are_images, if: -> { files.attached? }
4
+
5
+ private def files_are_images
6
+ errors.add :files, :invalid unless files.all?(&:image?)
7
+ end
8
+ end
9
+ 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,4 @@
1
+ module Documents
2
+ class PageBreak < Element
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Documents
2
+ class Paragraph < Element
3
+ attribute :html, :string, default: ""
4
+ validates :html, presence: true
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Documents
2
+ class PhoneValue < TextValue
3
+ validates :value, format: {with: /\A[\+\-\(\)\s\d\.]+\z/, message: :invalid_phone}, on: :update, allow_blank: true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Documents
2
+ class RichTextValue < TextValue
3
+ # Identical to TextValue - supports rich HTML content
4
+ end
5
+ 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,6 @@
1
+ module Documents
2
+ class Table < Element
3
+ include Container
4
+ attribute :column, :integer, default: 0
5
+ end
6
+ 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,5 @@
1
+ module Documents
2
+ class UrlValue < TextValue
3
+ validates :value, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: :invalid_url}, on: :update, allow_blank: true
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Documents
2
+ class Video < Element
3
+ attribute :url, :string, default: ""
4
+ validates :url, presence: true, format: /\A#{URI::RFC2396_REGEXP::PATTERN::ABS_URI}\Z/o
5
+ end
6
+ 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,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -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,11 @@
1
+ module Documents
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Documents
4
+
5
+ config.generators do |g|
6
+ g.test_framework :rspec
7
+ g.assets false
8
+ g.helper false
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,3 @@
1
+ module Documents
2
+ VERSION = "0.1.0"
3
+ 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"
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :documents do
3
+ # # Task goes here
4
+ # end
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: []