metadata_presenter 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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +68 -0
  4. data/Rakefile +29 -0
  5. data/app/assets/config/metadata_presenter_manifest.js +1 -0
  6. data/app/assets/stylesheets/metadata_presenter/application.css +15 -0
  7. data/app/controllers/metadata_presenter/answers_controller.rb +42 -0
  8. data/app/controllers/metadata_presenter/change_answer_controller.rb +8 -0
  9. data/app/controllers/metadata_presenter/engine_controller.rb +25 -0
  10. data/app/controllers/metadata_presenter/pages_controller.rb +14 -0
  11. data/app/controllers/metadata_presenter/service_controller.rb +8 -0
  12. data/app/controllers/metadata_presenter/submissions_controller.rb +13 -0
  13. data/app/helpers/metadata_presenter/application_helper.rb +7 -0
  14. data/app/jobs/metadata_presenter/application_job.rb +4 -0
  15. data/app/models/metadata_presenter/component.rb +5 -0
  16. data/app/models/metadata_presenter/metadata.rb +26 -0
  17. data/app/models/metadata_presenter/next_page.rb +18 -0
  18. data/app/models/metadata_presenter/page.rb +27 -0
  19. data/app/models/metadata_presenter/service.rb +28 -0
  20. data/app/validators/metadata_presenter/base_validator.rb +119 -0
  21. data/app/validators/metadata_presenter/max_length_validator.rb +7 -0
  22. data/app/validators/metadata_presenter/min_length_validator.rb +7 -0
  23. data/app/validators/metadata_presenter/required_validator.rb +7 -0
  24. data/app/validators/metadata_presenter/validate_answers.rb +48 -0
  25. data/app/validators/metadata_presenter/validate_schema.rb +39 -0
  26. data/app/views/errors/404.html +67 -0
  27. data/app/views/layouts/metadata_presenter/application.html.erb +33 -0
  28. data/app/views/metadata_presenter/component/_text.html.erb +7 -0
  29. data/app/views/metadata_presenter/header/show.html.erb +35 -0
  30. data/app/views/metadata_presenter/page/confirmation.html.erb +21 -0
  31. data/app/views/metadata_presenter/page/form.html.erb +19 -0
  32. data/app/views/metadata_presenter/page/singlequestion.html.erb +18 -0
  33. data/app/views/metadata_presenter/page/start.html.erb +38 -0
  34. data/app/views/metadata_presenter/page/summary.html.erb +73 -0
  35. data/config/initializers/default_metadata.rb +15 -0
  36. data/config/initializers/schemas.rb +13 -0
  37. data/config/routes.rb +8 -0
  38. data/lib/metadata_presenter.rb +4 -0
  39. data/lib/metadata_presenter/engine.rb +9 -0
  40. data/lib/metadata_presenter/version.rb +3 -0
  41. data/lib/tasks/metadata_presenter_tasks.rake +4 -0
  42. metadata +285 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b5fb97a4c652ee76cd80c505b82660463ac5b430d501a77299c61e7721bfdf8e
4
+ data.tar.gz: 84fae72ad0d433dbc6e519e08ec108680832fb72d19f5a2c404d739c062c61d5
5
+ SHA512:
6
+ metadata.gz: 92a7c904f51c6569e445b679796662426e3935f930d99a25bfaa9584a2b5da74444a6961ee0b104dfe8c9a4177a898b423ea3fd87db566b6e1c00d9ed0f84910
7
+ data.tar.gz: 1af429a8c6ef4e7b1d5d050d22e09bd247a48f5c63cf6afbcc01463555eedc2109867f5413639de32b80bc386bbc2e08e1b6c78c4658dc8d78b59f07302a0cbc
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Tomas D'Stefano
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,68 @@
1
+ # MetadataPresenter
2
+
3
+ Rails engine responsible for rendering the MoJ Form Builder metadata into
4
+ GOV.UK design system components.
5
+
6
+ ## Installation
7
+
8
+ ```ruby
9
+ gem 'metadata_presenter'
10
+ ```
11
+
12
+ And then execute:
13
+
14
+ ```bash
15
+ $ bundle
16
+ ```
17
+
18
+ Or install it yourself as:
19
+ ```bash
20
+ $ gem install metadata_presenter
21
+ ```
22
+
23
+ ## Setup & Mount
24
+
25
+ To mount the application:
26
+
27
+ ```ruby
28
+ mount MetadataPresenter::Engine => '/'
29
+ ```
30
+
31
+ Or if you are using for another route (like the MoJ Editor as preview feature):
32
+
33
+ ```ruby
34
+ mount MetadataPresenter::Engine => '/preview', as: :preview
35
+ ```
36
+
37
+ The MetadataPresenter controllers inherits from your main
38
+ `::ApplicationController` as default but you can overwrite if you need:
39
+
40
+ ```ruby
41
+ MetadataPresenter.parent_controller = '::MyAwesomeController'
42
+ ```
43
+
44
+ The application that you mount requires to save and load user data from some
45
+ store (session or a backend API or direct to a database). In order to do
46
+ that you need to write the following methods in your controller:
47
+
48
+ 1. save_user_data
49
+ 2. load_user_data
50
+
51
+ The user answers can be accessed via `params[:answers]`.
52
+
53
+ An example of implementation:
54
+ ```ruby
55
+ class MyAwesomeController
56
+ def save_user_data
57
+ session[:user_data] = params[:answers]
58
+ end
59
+
60
+ def load_user_data
61
+ session[:user_data]
62
+ end
63
+ end
64
+ ```
65
+
66
+ ## Generate documentation
67
+
68
+ Run `rake doc` and open the doc/index.html
@@ -0,0 +1,29 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'MetadataPresenter'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'yard'
18
+ YARD::Rake::YardocTask.new(:doc) do |yardoc|
19
+ yardoc.files = ['app/**/*.rb', 'lib/**/*.rb']
20
+ end
21
+
22
+ task default: :rspec
23
+
24
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
25
+ load 'rails/tasks/engine.rake'
26
+ load 'rails/tasks/statistics.rake'
27
+ Bundler::GemHelper.install_tasks
28
+
29
+ require 'bundler/gem_tasks'
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/metadata_presenter .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,42 @@
1
+ module MetadataPresenter
2
+ class AnswersController < EngineController
3
+ def create
4
+ if page.validate_answers(answers_params)
5
+ save_user_data # method signature
6
+ redirect_to_next_page
7
+ else
8
+ render_validation_error
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def page
15
+ @page ||= MetadataPresenter::Page.new(
16
+ service.find_page(params[:page_url]).metadata
17
+ )
18
+ end
19
+
20
+ def redirect_to_next_page
21
+ next_page = NextPage.new(service).find(
22
+ session: session,
23
+ current_page_url: params[:page_url]
24
+ )
25
+
26
+ if next_page.present?
27
+ redirect_to_page next_page.url
28
+ else
29
+ not_found
30
+ end
31
+ end
32
+
33
+ def render_validation_error
34
+ @user_data = answers_params
35
+ render template: page.template, status: :unprocessable_entity
36
+ end
37
+
38
+ def answers_params
39
+ params[:answers] ? params[:answers].permit! : {}
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,8 @@
1
+ module MetadataPresenter
2
+ class ChangeAnswerController < EngineController
3
+ def create
4
+ session[:return_to_check_you_answer] = true
5
+ redirect_to_page params[:url]
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ module MetadataPresenter
2
+ class EngineController < MetadataPresenter.parent_controller.constantize
3
+ protect_from_forgery with: :exception
4
+
5
+ helper MetadataPresenter::ApplicationHelper
6
+ default_form_builder GOVUKDesignSystemFormBuilder::FormBuilder
7
+
8
+ def back_link
9
+ return if @page.blank?
10
+
11
+ @back_link ||= service.previous_page(current_page: @page)&.url
12
+ end
13
+ helper_method :back_link
14
+
15
+ private
16
+
17
+ def not_found
18
+ render template: 'errors/404', status: 404
19
+ end
20
+
21
+ def redirect_to_page(url)
22
+ redirect_to File.join(request.script_name, url)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ module MetadataPresenter
2
+ class PagesController < EngineController
3
+ def show
4
+ @user_data = load_user_data # method signature
5
+ @page ||= service.find_page(request.env['PATH_INFO'])
6
+
7
+ if @page
8
+ render template: @page.template
9
+ else
10
+ not_found
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module MetadataPresenter
2
+ class ServiceController < EngineController
3
+ def start
4
+ @page = service.start_page
5
+ render template: @page.template
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module MetadataPresenter
2
+ class SubmissionsController < EngineController
3
+ def create
4
+ @page = service.confirmation_page
5
+
6
+ if @page
7
+ redirect_to_page @page.url
8
+ else
9
+ not_found
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module MetadataPresenter
2
+ module ApplicationHelper
3
+ def to_markdown(text)
4
+ (Kramdown::Document.new(text).to_html).html_safe
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module MetadataPresenter
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ class MetadataPresenter::Component < MetadataPresenter::Metadata
2
+ def to_partial_path
3
+ "component/#{type}"
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ class MetadataPresenter::Metadata
2
+ include ActiveModel::Conversion
3
+ extend ActiveModel::Naming
4
+
5
+ attr_reader :metadata
6
+
7
+ def initialize(metadata)
8
+ @metadata = OpenStruct.new(metadata)
9
+ end
10
+
11
+ def id
12
+ metadata._id
13
+ end
14
+
15
+ def type
16
+ metadata._type
17
+ end
18
+
19
+ def respond_to_missing?(method_name, include_private = false)
20
+ metadata.respond_to?(method_name)
21
+ end
22
+
23
+ def method_missing(method, *args, &block)
24
+ metadata.send(method, *args, &block)
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ module MetadataPresenter
2
+ class NextPage
3
+ attr_reader :service
4
+
5
+ def initialize(service)
6
+ @service = service
7
+ end
8
+
9
+ def find(session:, current_page_url:)
10
+ if session[:return_to_check_you_answer].present?
11
+ session[:return_to_check_you_answer] = nil
12
+ service.pages.find { |page| page.type == 'page.summary' }
13
+ else
14
+ service.next_page(from: current_page_url)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ module MetadataPresenter
2
+ class Page < MetadataPresenter::Metadata
3
+ include ActiveModel::Validations
4
+
5
+ def validate_answers(answers)
6
+ ValidateAnswers.new(page: self, answers: answers).valid?
7
+ end
8
+
9
+ def ==(other)
10
+ id == other.id if other.respond_to? :id
11
+ end
12
+
13
+ def components
14
+ metadata.components&.map do |component|
15
+ MetadataPresenter::Component.new(component)
16
+ end
17
+ end
18
+
19
+ def to_partial_path
20
+ type.gsub('.', '/')
21
+ end
22
+
23
+ def template
24
+ "metadata_presenter/#{type.gsub('.', '/')}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ class MetadataPresenter::Service < MetadataPresenter::Metadata
2
+ def pages
3
+ @_pages ||= metadata.pages.map { |page| MetadataPresenter::Page.new(page) }
4
+ end
5
+
6
+ def start_page
7
+ pages.first
8
+ end
9
+
10
+ def find_page(path)
11
+ pages.find { |page| page.url == path }
12
+ end
13
+
14
+ def next_page(from:)
15
+ current_page = find_page(from)
16
+ pages[pages.index(current_page) + 1] if current_page.present?
17
+ end
18
+
19
+ def previous_page(current_page:)
20
+ pages[pages.index(current_page) - 1] unless current_page == start_page
21
+ end
22
+
23
+ def confirmation_page
24
+ @confirmation_page ||= pages.find do |page|
25
+ page.type == 'page.confirmation'
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,119 @@
1
+ module MetadataPresenter
2
+ class NoDefaultMessage < StandardError; end
3
+
4
+ # Abstract base class for validation utilities.
5
+ # Provides an interface for implementing validation from the metadata.
6
+ #
7
+ # @abstract
8
+ #
9
+ # The Base validator expects the subclass to implement only #invalid_answer?
10
+ # as long the conventions are followed:
11
+ #
12
+ # 1. The default metadata for error messages follows the "error.name_of_the_class_without_validator"
13
+ # 2. The class should have the same name as the schema e.g 'required' will lookup for RequiredValidator.
14
+ #
15
+ # On the example below the base validator will look for the custom message
16
+ # on "errors" -> "grogu" -> "any" and if there is none, then will
17
+ # look for the default message on default metadata as "error.grogu".
18
+ #
19
+ # @example Custom validator
20
+ # class GroguValidator < BaseValidator
21
+ # def invalid_answer?
22
+ # user_answer = answers[component.name]
23
+ #
24
+ # user_answer.to_s == 'Grogu'
25
+ # end
26
+ # end
27
+ #
28
+ class BaseValidator
29
+ # @return [MetadataPresenter::Page] page object from the metadata
30
+ attr_reader :page
31
+
32
+ # @return [Hash] the user answers
33
+ attr_reader :answers
34
+
35
+ # @return [MetadataPresenter::Component] component object from the metadata
36
+ attr_reader :component
37
+
38
+ def initialize(page:, answers:, component:)
39
+ @page = page
40
+ @answers = answers
41
+ @component = component
42
+ end
43
+
44
+ def valid?
45
+ if invalid_answer?
46
+ error_message = custom_error_message || default_error_message
47
+ page.errors.add(component.id, error_message)
48
+ end
49
+
50
+ page.errors.blank?
51
+ end
52
+
53
+ # The custom message will be lookup from the schema key on the metadata.
54
+ # Assuming for example that the schema key is 'grogu' then the message
55
+ # will lookup for 'errors.grogu.any'.
56
+ #
57
+ # @return [String] message from the service metadata
58
+ #
59
+ def custom_error_message
60
+ message = component.dig('errors', schema_key, 'any')
61
+
62
+ message % error_message_hash if message.present?
63
+ end
64
+
65
+ # The default error message will be look using the schema key.
66
+ # Assuming the schema key is 'grogu' then the default message
67
+ # will look for 'error.grogu.value'.
68
+ #
69
+ # @return [String] returns the default error message
70
+ # @raise [MetadataPresenter::NoDefaultMessage] raises no default message if
71
+ # is not present
72
+ def default_error_message
73
+ default_error_message_key = "error.#{schema_key}"
74
+ default_message = Rails
75
+ .application
76
+ .config
77
+ .default_metadata[default_error_message_key]
78
+
79
+ if default_message.present?
80
+ default_message['value'] % error_message_hash
81
+ else
82
+ raise NoDefaultMessage, "No default message found for key '#{default_error_message_key}'."
83
+ end
84
+ end
85
+
86
+ # Needs to be implemented on the subclass
87
+ #
88
+ # @return [TrueClass] if is invalid
89
+ # @return [FalseClass] if is valid
90
+ #
91
+ def invalid_answer?
92
+ raise NotImplementedError
93
+ end
94
+
95
+ # The convention to be looked on the metadata is by the name of the class.
96
+ # E.g the GroguValidator will look for 'grogu' on the metadata.
97
+ # Overwrite this method if the validator doesn't follow the convetions.
98
+ #
99
+ # @return [String] schema key to be looked on the metadata and in the
100
+ # default metadata
101
+ #
102
+ def schema_key
103
+ @schema_key ||= self.class.name.demodulize.gsub('Validator', '').underscore
104
+ end
105
+
106
+ # Error message hash that will be interpolate with the custom message or
107
+ # the default metadata
108
+ #
109
+ # The message could include '%{control}' to add the label name.
110
+ # Or for the GroguValidator will be '%{grogu}' and the value setup in the metadata.
111
+ #
112
+ def error_message_hash
113
+ {
114
+ control: component.label,
115
+ schema_key.to_sym => component.validation[schema_key]
116
+ }
117
+ end
118
+ end
119
+ end