metadata_presenter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +68 -0
- data/Rakefile +29 -0
- data/app/assets/config/metadata_presenter_manifest.js +1 -0
- data/app/assets/stylesheets/metadata_presenter/application.css +15 -0
- data/app/controllers/metadata_presenter/answers_controller.rb +42 -0
- data/app/controllers/metadata_presenter/change_answer_controller.rb +8 -0
- data/app/controllers/metadata_presenter/engine_controller.rb +25 -0
- data/app/controllers/metadata_presenter/pages_controller.rb +14 -0
- data/app/controllers/metadata_presenter/service_controller.rb +8 -0
- data/app/controllers/metadata_presenter/submissions_controller.rb +13 -0
- data/app/helpers/metadata_presenter/application_helper.rb +7 -0
- data/app/jobs/metadata_presenter/application_job.rb +4 -0
- data/app/models/metadata_presenter/component.rb +5 -0
- data/app/models/metadata_presenter/metadata.rb +26 -0
- data/app/models/metadata_presenter/next_page.rb +18 -0
- data/app/models/metadata_presenter/page.rb +27 -0
- data/app/models/metadata_presenter/service.rb +28 -0
- data/app/validators/metadata_presenter/base_validator.rb +119 -0
- data/app/validators/metadata_presenter/max_length_validator.rb +7 -0
- data/app/validators/metadata_presenter/min_length_validator.rb +7 -0
- data/app/validators/metadata_presenter/required_validator.rb +7 -0
- data/app/validators/metadata_presenter/validate_answers.rb +48 -0
- data/app/validators/metadata_presenter/validate_schema.rb +39 -0
- data/app/views/errors/404.html +67 -0
- data/app/views/layouts/metadata_presenter/application.html.erb +33 -0
- data/app/views/metadata_presenter/component/_text.html.erb +7 -0
- data/app/views/metadata_presenter/header/show.html.erb +35 -0
- data/app/views/metadata_presenter/page/confirmation.html.erb +21 -0
- data/app/views/metadata_presenter/page/form.html.erb +19 -0
- data/app/views/metadata_presenter/page/singlequestion.html.erb +18 -0
- data/app/views/metadata_presenter/page/start.html.erb +38 -0
- data/app/views/metadata_presenter/page/summary.html.erb +73 -0
- data/config/initializers/default_metadata.rb +15 -0
- data/config/initializers/schemas.rb +13 -0
- data/config/routes.rb +8 -0
- data/lib/metadata_presenter.rb +4 -0
- data/lib/metadata_presenter/engine.rb +9 -0
- data/lib/metadata_presenter/version.rb +3 -0
- data/lib/tasks/metadata_presenter_tasks.rake +4 -0
- metadata +285 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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,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,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
|