metadata_presenter 0.1.0

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