metadata_presenter 3.0.15 → 3.2.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/metadata_presenter/answers_controller.rb +107 -5
  3. data/app/controllers/metadata_presenter/file_controller.rb +9 -0
  4. data/app/controllers/metadata_presenter/pages_controller.rb +1 -2
  5. data/app/helpers/metadata_presenter/application_helper.rb +56 -0
  6. data/app/models/metadata_presenter/component.rb +10 -1
  7. data/app/models/metadata_presenter/file_uploader.rb +4 -0
  8. data/app/models/metadata_presenter/multi_upload_answer.rb +33 -0
  9. data/app/models/metadata_presenter/page.rb +4 -0
  10. data/app/models/metadata_presenter/page_answers.rb +60 -1
  11. data/app/presenters/metadata_presenter/page_answers_presenter.rb +4 -0
  12. data/app/validators/metadata_presenter/accept_validator.rb +11 -0
  13. data/app/validators/metadata_presenter/max_files_validator.rb +9 -0
  14. data/app/validators/metadata_presenter/multiupload_validator.rb +24 -0
  15. data/app/validators/metadata_presenter/required_validator.rb +4 -0
  16. data/app/validators/metadata_presenter/validate_answers.rb +15 -6
  17. data/app/views/metadata_presenter/component/_multiupload.html.erb +51 -0
  18. data/app/views/metadata_presenter/page/confirmation.html.erb +2 -0
  19. data/app/views/metadata_presenter/page/content.html.erb +4 -1
  20. data/config/initializers/supported_components.rb +1 -1
  21. data/config/locales/en.yml +12 -0
  22. data/config/routes.rb +1 -0
  23. data/default_metadata/component/multiupload.json +30 -0
  24. data/default_metadata/page/content.json +0 -1
  25. data/default_metadata/string/error.max_files.json +6 -0
  26. data/default_metadata/string/error.multiupload.json +6 -0
  27. data/default_metadata/validations/max_files.json +1 -0
  28. data/fixtures/version.json +73 -0
  29. data/lib/metadata_presenter/version.rb +1 -1
  30. data/schemas/component/content.json +29 -0
  31. data/schemas/component/multiupload.json +42 -0
  32. data/schemas/definition/expression_item.json +36 -0
  33. data/schemas/definition/expressions.json +15 -0
  34. data/schemas/flow/branch.json +1 -27
  35. data/schemas/page/confirmation.json +0 -3
  36. data/schemas/validations/validations.json +38 -0
  37. metadata +27 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 748d688aee936ddbfc93a68c3ee6d658938987562b238f815ef852184751d26d
4
- data.tar.gz: 509757afa039a05d98e142e8fb47930063063af1c2117e8b9cbb2a317a7b198e
3
+ metadata.gz: e690363ac373962c0478469b6b501d6ecc255473d6bbd04aff6cf012aa098843
4
+ data.tar.gz: 907eced7e4a09a2b247130deb082ef5c05766dd3d995d18483c69d5dcb8babf7
5
5
  SHA512:
6
- metadata.gz: fe3620821903ecd6d4f44a9cf6535a9c8dbba7bfc03da4e92b6e19557e423058d549f1edcf8c1124aba9e72042c410e16b997e2e14203a82cc112211de1e9904
7
- data.tar.gz: 34fe18437640f2cccdfebfb04c67164181b2c00bf98d549719bf62b0b02bd8478211ed12f464c9105ee587e3cc325ae44a67a98b95f7a62ed21992bf430b92d8
6
+ metadata.gz: a061114f4db7608516af2da01234bedd45b11708c47c9f693ebffa0b13d7f1fe2814313ce79bfc4bf555b2db3526d63743193789297e1bf2f3d8a87ec6611a40
7
+ data.tar.gz: 78b39e5035fe137a52b83c2b3f6a42edcc72c2d921247a67f6958608acf2a5df8ba15795986e56f5d75e602c530a1d83005c30a89d51c0ce1362f2ce9984be91
@@ -4,30 +4,85 @@ module MetadataPresenter
4
4
 
5
5
  def create
6
6
  @previous_answers = reload_user_data.deep_dup
7
- @page_answers = PageAnswers.new(page, answers_params, autocomplete_items(page.components))
7
+
8
+ @page_answers = PageAnswers.new(page, incoming_answer, autocomplete_items(page.components))
8
9
 
9
10
  if params[:save_for_later].present?
10
- save_user_data
11
- # NOTE: if the user is on a file upload page, files will not be uploaded before redirection
11
+ save_user_data unless upload? || multiupload?
12
+
12
13
  redirect_to save_path(page_slug: params[:page_slug]) and return
13
14
  end
14
15
 
15
16
  upload_files if upload?
17
+ upload_multiupload_new_files if multiupload? && answers_params.present?
16
18
 
17
19
  if @page_answers.validate_answers
18
20
  save_user_data # method signature
21
+
22
+ # if adding another file in multi upload, redirect back to referrer
23
+ if about_to_render_multiupload?
24
+ redirect_back(fallback_location: root_path) and return
25
+ end
26
+
19
27
  redirect_to_next_page
20
28
  else
29
+ # can't render error in the same way for the multiupload component
30
+ if about_to_render_multiupload?
31
+ @user_data = @previous_answers
32
+
33
+ render template: @page.template, status: :unprocessable_entity and return
34
+ end
21
35
  render_validation_error
22
36
  end
23
37
  end
24
38
 
39
+ def about_to_render_multiupload?
40
+ answers_params.present? && multiupload?
41
+ end
42
+
25
43
  def update_count_matching_filenames(original_filename, user_data)
26
44
  extname = File.extname(original_filename)
27
45
  basename = File.basename(original_filename, extname)
28
46
  filename_regex = /^#{Regexp.quote(basename)}(?>-\((\d)\))?#{Regexp.quote(extname)}/
29
47
 
30
- user_data.select { |_k, v| v.instance_of?(Hash) && v['original_filename'] =~ filename_regex }.count
48
+ user_data.select { |_k, v|
49
+ if v.is_a?(Array)
50
+ v.any? { |e| e['original_filename'] =~ filename_regex }
51
+ else
52
+ v['original_filename'] =~ filename_regex
53
+ end
54
+ }.count
55
+ end
56
+
57
+ def upload_multiupload_new_files
58
+ user_data = load_user_data
59
+ @page_answers.page.multiupload_components.each do |component|
60
+ previous_answers = user_data[component.id]
61
+ incoming_filename = @page_answers.send(component.id)[component.id].last['original_filename']
62
+
63
+ if editor_preview?
64
+ @page_answers.uploaded_files.push(multiuploaded_file(previous_answers, component))
65
+ else
66
+
67
+ if incoming_filename.present?
68
+ # determine if duplicate filename from any other user answer
69
+ @page_answers.count = update_count_matching_filenames(incoming_filename, user_data)
70
+ end
71
+
72
+ if previous_answers.present? && previous_answers.any? { |answer| answer['original_filename'] == incoming_answer.incoming_answer.values.first.original_filename }
73
+ @page_answers.count = nil # ensure we don't also try to suffix this filename as we will reject it anyway
74
+ file = MetadataPresenter::UploadedFile.new(
75
+ file: @page_answers.send(component.id)[component.id].last,
76
+ component:
77
+ )
78
+
79
+ file.errors.add('invalid.multiupload')
80
+ @page_answers.uploaded_files.push(file)
81
+ else
82
+ @page_answers.uploaded_files.push(multiuploaded_file(previous_answers, component))
83
+ end
84
+ end
85
+ end
31
86
  end
32
87
 
33
88
  def show_save_and_return
@@ -67,6 +122,16 @@ module MetadataPresenter
67
122
  render template: page.template, status: :unprocessable_entity
68
123
  end
69
124
 
125
+ def incoming_answer
126
+ if multiupload?
127
+ multiupload_answer = MultiUploadAnswer.new
128
+ multiupload_answer.key = Array(page.components).first.id
129
+ multiupload_answer.previous_answers = @previous_answers[Array(page.components).first.id]
130
+ multiupload_answer.incoming_answer = answers_params
131
+ end
132
+ multiupload_answer || answers_params
133
+ end
134
+
70
135
  def answers_params
71
136
  params.permit(:page_slug, :save_for_later)
72
137
  params[:answers] ? params[:answers].permit! : {}
@@ -84,7 +149,6 @@ module MetadataPresenter
84
149
  user_data = load_user_data
85
150
  @page_answers.page.upload_components.each do |component|
86
151
  answer = user_data[component.id]
87
-
88
152
  original_filename = answer.nil? ? @page_answers.send(component.id)['original_filename'] : answer['original_filename']
89
153
 
90
154
  if original_filename.present?
@@ -112,6 +176,40 @@ module MetadataPresenter
112
176
  end
113
177
  end
114
178
 
179
+ def multiuploaded_file(answer, component)
180
+ if answer.present?
181
+ if @page_answers.answers.is_a?(MetadataPresenter::MultiUploadAnswer)
182
+ if @page_answers.answers.incoming_answer.present?
183
+ FileUploader.new(
184
+ session:,
185
+ page_answers: @page_answers,
186
+ component:,
187
+ adapter: upload_adapter
188
+ ).upload
189
+ else
190
+ MetadataPresenter::UploadedFile.new(
191
+ file: @page_answers.answers.previous_answers.last,
192
+ component:
193
+ )
194
+ end
195
+ else
196
+ FileUploader.new(
197
+ session:,
198
+ page_answers: @page_answers,
199
+ component:,
200
+ adapter: upload_adapter
201
+ ).upload
202
+ end
203
+ else
204
+ FileUploader.new(
205
+ session:,
206
+ page_answers: @page_answers,
207
+ component:,
208
+ adapter: upload_adapter
209
+ ).upload
210
+ end
211
+ end
212
+
115
213
  def upload_adapter
116
214
  super if defined?(super)
117
215
  end
@@ -119,5 +217,9 @@ module MetadataPresenter
119
217
  def upload?
120
218
  Array(page.components).any?(&:upload?)
121
219
  end
220
+
221
+ def multiupload?
222
+ Array(page.components).any?(&:multiupload?)
223
+ end
122
224
  end
123
225
  end
@@ -5,8 +5,17 @@ module MetadataPresenter
5
5
  redirect_back(fallback_location: root_path)
6
6
  end
7
7
 
8
+ def remove_multifile
9
+ remove_file_from_data(params[:component_id], params[:file_uuid])
10
+ redirect_back(fallback_location: root_path)
11
+ end
12
+
8
13
  def remove_user_data(component_id)
9
14
  super(component_id) if defined?(super)
10
15
  end
16
+
17
+ def remove_file_from_data(component_id, file_id)
18
+ super(component_id, file_id) if defined?(super)
19
+ end
11
20
  end
12
21
  end
@@ -4,13 +4,12 @@ module MetadataPresenter
4
4
 
5
5
  def show
6
6
  @user_data = load_user_data # method signature
7
- @page ||= service.find_page_by_url(request.env['PATH_INFO'])
8
7
 
8
+ @page ||= service.find_page_by_url(request.env['PATH_INFO'])
9
9
  if @page
10
10
  load_autocomplete_items
11
11
 
12
12
  @page_answers = PageAnswers.new(@page, @user_data)
13
-
14
13
  render template: @page.template
15
14
  else
16
15
  not_found
@@ -35,5 +35,61 @@ module MetadataPresenter
35
35
  def default_page_title(type)
36
36
  MetadataPresenter::DefaultMetadata[type.to_s]&.[]('heading')
37
37
  end
38
+
39
+ def multiupload_files_remaining
40
+ component = page_multiupload_component
41
+ answers = @user_data.keys.include?(component.id) ? @user_data.find(component.id).first : []
42
+ max_files = component.validation['max_files'].to_i
43
+
44
+ if uploads_remaining.zero?
45
+ I18n.t('presenter.questions.multiupload.none')
46
+ elsif max_files == 1
47
+ I18n.t('presenter.questions.multiupload.single_upload')
48
+ elsif uploads_remaining == 1
49
+ if answers.present?
50
+ I18n.t('presenter.questions.multiupload.answered_singular')
51
+ else
52
+ I18n.t('presenter.questions.multiupload.singular')
53
+ end
54
+ elsif answers.present?
55
+ I18n.t('presenter.questions.multiupload.answered_plural', num: uploads_remaining)
56
+ else
57
+ I18n.t('presenter.questions.multiupload.plural', num: uploads_remaining)
58
+ end
59
+ end
60
+
61
+ def uploads_remaining
62
+ component = page_multiupload_component
63
+ max_files = component.validation['max_files'].to_i
64
+ answers = @user_data.keys.include?(component.id) ? @user_data[component.id] : []
65
+ return 0 if answers.is_a?(ActionDispatch::Http::UploadedFile)
66
+
67
+ max_files - answers.count
68
+ end
69
+
70
+ def uploads_count
71
+ component = page_multiupload_component
72
+ answers = @user_data.keys.include?(component.id) ? @user_data[component.id] : []
73
+
74
+ return 0 if answers.is_a?(ActionDispatch::Http::UploadedFile)
75
+
76
+ answers.count == 1 ? I18n.t('presenter.questions.multiupload.answered_count_singular') : I18n.t('presenter.questions.multiupload.answered_count_plural', num: answers.count)
77
+ end
78
+
79
+ def files_to_render
80
+ component = page_multiupload_component
81
+
82
+ error_file = @page_answers.uploaded_files.select { |file| file.errors.any? }.first
83
+
84
+ if error_file.present?
85
+ @page_answers.send(component.id)[component.id].compact.reject { |file| file[error_file.file['original_filename'] == 'original_filename'] }
86
+ else
87
+ @page_answers.send(component.id)[component.id].compact
88
+ end
89
+ end
90
+
91
+ def page_multiupload_component
92
+ @page.components.select { |c| c.type == 'multiupload' }.first
93
+ end
38
94
  end
39
95
  end
@@ -3,7 +3,8 @@ class MetadataPresenter::Component < MetadataPresenter::Metadata
3
3
  'date' => 'date',
4
4
  'number' => 'number',
5
5
  'text' => 'string',
6
- 'textarea' => 'string'
6
+ 'textarea' => 'string',
7
+ 'multiupload' => 'file'
7
8
  }.freeze
8
9
 
9
10
  # Used for max_length and max_word validations.
@@ -57,6 +58,10 @@ class MetadataPresenter::Component < MetadataPresenter::Metadata
57
58
  type == 'upload'
58
59
  end
59
60
 
61
+ def multiupload?
62
+ type == 'multiupload'
63
+ end
64
+
60
65
  def find_item_by_uuid(uuid)
61
66
  items.find { |item| item.uuid == uuid }
62
67
  end
@@ -73,6 +78,10 @@ class MetadataPresenter::Component < MetadataPresenter::Metadata
73
78
  VALIDATION_STRING_LENGTH_THRESHOLD
74
79
  end
75
80
 
81
+ def max_files
82
+ metadata.max_files.presence || '0'
83
+ end
84
+
76
85
  private
77
86
 
78
87
  def validation_bundle_key
@@ -18,6 +18,10 @@ module MetadataPresenter
18
18
  end
19
19
 
20
20
  def file_details
21
+ if component.multiupload?
22
+ return page_answers.send(component.id)[component.id].last
23
+ end
24
+
21
25
  page_answers.send(component.id)
22
26
  end
23
27
  end
@@ -0,0 +1,33 @@
1
+ module MetadataPresenter
2
+ class MultiUploadAnswer
3
+ attr_accessor :previous_answers, :incoming_answer, :key
4
+
5
+ def to_h
6
+ {
7
+ key => previous_answers_value.present? ? previous_answers_value.reject(&:blank?) : []
8
+ }
9
+ end
10
+
11
+ def previous_answers_value
12
+ return nil if previous_answers.nil? && incoming_answer.nil?
13
+ return [incoming_answer] if previous_answers.nil? && incoming_answer.present?
14
+
15
+ if previous_answers.is_a?(Array)
16
+ return previous_answers.reject(&:blank?) if incoming_answer.nil? || previous_answers.find { |answer|
17
+ answer['original_filename'] == incoming_answer['original_filename']
18
+ }.present?
19
+
20
+ previous_answers.reject(&:blank?).push(incoming_answer)
21
+ else
22
+ return [previous_answers] if incoming_answer.nil?
23
+
24
+ [previous_answers, incoming_answer]
25
+ end
26
+ end
27
+
28
+ def from_h(input)
29
+ self.key = input.keys[0]
30
+ self.previous_answers = input[key]
31
+ end
32
+ end
33
+ end
@@ -85,6 +85,10 @@ module MetadataPresenter
85
85
  components.select(&:upload?)
86
86
  end
87
87
 
88
+ def multiupload_components
89
+ components.select(&:multiupload?)
90
+ end
91
+
88
92
  def standalone?
89
93
  type == 'page.standalone'
90
94
  end
@@ -25,11 +25,13 @@ module MetadataPresenter
25
25
 
26
26
  def method_missing(method_name, *_args)
27
27
  component = components.find { |c| c.id == method_name.to_s }
28
-
29
28
  if component && component.type == 'date'
30
29
  date_answer(component.id)
31
30
  elsif component && component.type == 'upload'
32
31
  upload_answer(component.id, count)
32
+ elsif component && component.type == 'multiupload'
33
+ answer_object = multiupload_answer(component.id, count)
34
+ answer_object.to_h if answer_object.present?
33
35
  elsif component && component.type == 'checkboxes'
34
36
  answers[method_name.to_s].to_a
35
37
  else
@@ -53,6 +55,62 @@ module MetadataPresenter
53
55
  end
54
56
  end
55
57
 
58
+ def multiupload_answer(component_id, _count)
59
+ file_details = answers[component_id.to_s] unless answers.is_a?(MetadataPresenter::MultiUploadAnswer)
60
+ return nil if file_details.nil? && answers.nil?
61
+
62
+ if file_details.is_a?(Hash)
63
+ # when referencing a single previous answer but no incoming new answer
64
+ presentable = MetadataPresenter::MultiUploadAnswer.new
65
+ presentable.key = component_id.to_s
66
+ presentable.previous_answers = [file_details]
67
+ return presentable
68
+ end
69
+
70
+ if file_details.is_a?(Array)
71
+ # when referencing multiple previous answers but no incoming new answer
72
+ presentable = MetadataPresenter::MultiUploadAnswer.new
73
+ presentable.key = component_id.to_s
74
+ presentable.previous_answers = file_details.reject { |f| f['original_filename'].blank? }
75
+ return presentable
76
+ end
77
+
78
+ if answers.blank?
79
+ return nil
80
+ end
81
+
82
+ if answers.is_a?(Hash) # rendering only existing answers
83
+ return if answers[component_id].blank?
84
+
85
+ if answers[component_id].is_a?(Array)
86
+ answers[component_id].each { |answer| answer['original_filename'] = sanitize(filename(update_filename(answer['original_filename']))) }
87
+ end
88
+
89
+ answers[component_id] = answers[component_id].reject { |a| a['original_filename'].blank? }
90
+ return answers
91
+ end
92
+
93
+ # uploading a new answer, this method will be called during multiple render operations
94
+ if answers.incoming_answer.present? && answers.incoming_answer.is_a?(ActionController::Parameters)
95
+ answers.incoming_answer[component_id].original_filename = sanitize(filename(update_filename(answers.incoming_answer[component_id].original_filename)))
96
+ end
97
+
98
+ if answers.incoming_answer.present? && answers.incoming_answer.is_a?(Hash)
99
+ answers.incoming_answer['original_filename'] = sanitize(filename(update_filename(answers.incoming_answer['original_filename'])))
100
+ end
101
+
102
+ if answers.incoming_answer.present? && answers.incoming_answer[component_id].is_a?(ActionDispatch::Http::UploadedFile)
103
+ answers.incoming_answer = {
104
+ 'original_filename' => sanitize(filename(update_filename(answers.incoming_answer[component_id].original_filename))),
105
+ 'content_type' => answers.incoming_answer[component_id].content_type,
106
+ 'tempfile' => answers.incoming_answer[component_id].tempfile.path.to_s,
107
+ 'uuid' => SecureRandom.uuid
108
+ }
109
+ end
110
+
111
+ answers
112
+ end
113
+
56
114
  def date_answer(component_id)
57
115
  date = raw_date_answer(component_id)
58
116
 
@@ -81,6 +139,7 @@ module MetadataPresenter
81
139
  basename = File.basename(filename, extname)
82
140
 
83
141
  filename = "#{basename}-(#{count})#{extname}"
142
+ @count = nil # this is called multiple times for multiupload components so ensure we apply suffix to filename only once
84
143
  end
85
144
 
86
145
  filename
@@ -84,6 +84,10 @@ module MetadataPresenter
84
84
  file_hash['original_filename']
85
85
  end
86
86
 
87
+ def multiupload(multifile_hash)
88
+ multifile_hash[component.id].map { |i| i['original_filename'] }.join('<br>').html_safe
89
+ end
90
+
87
91
  def autocomplete(value)
88
92
  JSON.parse(value)['text']
89
93
  end
@@ -3,5 +3,16 @@ module MetadataPresenter
3
3
  def error_name
4
4
  'accept'
5
5
  end
6
+
7
+ def error_message_hash
8
+ if component.type == 'multiupload'
9
+ {
10
+ control: page_answers.send(component.id)[component.id].last['original_filename'],
11
+ schema_key.to_sym => component.validation[schema_key]
12
+ }
13
+ else
14
+ super
15
+ end
16
+ end
6
17
  end
7
18
  end
@@ -0,0 +1,9 @@
1
+ module MetadataPresenter
2
+ class MaxFilesValidator < NumberValidator
3
+ def invalid_answer?
4
+ return if super
5
+
6
+ Float(user_answer, exception: false) > Float(component.validation[schema_key], exception: false)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ module MetadataPresenter
2
+ class MultiuploadValidator < BaseValidator
3
+ def invalid_answer?
4
+ user_answer.errors.any? { |error| error.attribute.to_s == error_name }
5
+ end
6
+
7
+ def user_answer
8
+ page_answers.uploaded_files.find do |uploaded_file|
9
+ component.id == uploaded_file.component.id
10
+ end
11
+ end
12
+
13
+ def error_message_hash
14
+ {
15
+ control: page_answers.send(component.id)[component.id].last['original_filename'],
16
+ schema_key.to_sym => component.validation[schema_key]
17
+ }
18
+ end
19
+
20
+ def error_name
21
+ 'invalid.multiupload'
22
+ end
23
+ end
24
+ end
@@ -1,6 +1,10 @@
1
1
  module MetadataPresenter
2
2
  class RequiredValidator < BaseValidator
3
3
  def invalid_answer?
4
+ if component.type == 'multiupload'
5
+ return user_answer[component.id].map(&:blank?).all?
6
+ end
7
+
4
8
  user_answer.blank?
5
9
  end
6
10
  end
@@ -21,12 +21,21 @@ module MetadataPresenter
21
21
  def validators
22
22
  components.map { |component|
23
23
  component_validations(component).map do |key|
24
- "MetadataPresenter::#{key.classify}Validator".constantize.new(
25
- **{
26
- page_answers:,
27
- component:
28
- }.merge(autocomplete_param(key))
29
- )
24
+ if key == 'max_files'
25
+ 'MetadataPresenter::MaxFilesValidator'.constantize.new(
26
+ **{
27
+ page_answers:,
28
+ component:
29
+ }.merge(autocomplete_param(key))
30
+ )
31
+ else
32
+ "MetadataPresenter::#{key.classify}Validator".constantize.new(
33
+ **{
34
+ page_answers:,
35
+ component:
36
+ }.merge(autocomplete_param(key))
37
+ )
38
+ end
30
39
  end
31
40
  }.compact.flatten
32
41
  end
@@ -0,0 +1,51 @@
1
+ <legend class="govuk-heading-xl"><%= input_title %></legend>
2
+
3
+ <% if answered?(component.id) && @page_answers.send(component.id)[component.id].compact.count.positive? %>
4
+ <label id="uploaded-file-summary-list-label"><p class="govuk-heading-s"><%= uploads_count %></p></label>
5
+
6
+ <dl id="uploaded-file-summary-list" class="fb-block fb-block-answers govuk-summary-list" aria-labelled-by="uploaded-file-summary-list-label">
7
+ <% files_to_render.each do |previous_file| %>
8
+ <div class="govuk-summary-list__row">
9
+ <dt class="govuk-summary-list__value">
10
+ <%= previous_file['original_filename'] %>
11
+ </dt>
12
+ <dd class="govuk-summary-list__actions">
13
+ <%= link_to "#{t('presenter.questions.multiupload.remove_file')}<span class=\"govuk-visually-hidden\">#{previous_file['original_filename']}</span>".html_safe, remove_multifile_path(component.id, previous_file['uuid']), class: 'govuk-link' %>
14
+ </dd>
15
+ </div>
16
+ <% end %>
17
+ </dl>
18
+ <% end %>
19
+
20
+ <% if uploads_remaining.positive? || editable? %>
21
+ <div data-multiupload-element="upload-another-file" <%= answered?(component.id) && @page_answers.send(component.id)[component.id].compact.count.positive? ? 'hidden' : '' %>>
22
+ <%= f.govuk_file_field component.id.to_sym,
23
+ hint: {
24
+ data: { "fb-default-text" => default_text('option_hint') },
25
+ text: component.hint.present? ? component.hint : ''
26
+ },
27
+ accept: component.validation['accept'],
28
+ disabled: editable?,
29
+ label: -> do %>
30
+ <% if answered?(component.id) && @page_answers.send(component.id)[component.id].compact.count.positive? && !editable? %>
31
+ <h3 class="govuk-heading-s govuk-!-margin-top-8"><%= t('presenter.questions.multiupload.add_another') %></h3>
32
+ <% else %>
33
+ <h3 class="govuk-visually-hidden"><%= t('presenter.questions.multiupload.add_another') %></h3>
34
+ <% end %>
35
+ <% end %>
36
+ </div>
37
+ <% end %>
38
+
39
+ <% if editable? %>
40
+ <% if editor_preview? && answered?(component.id) %>
41
+ <p class="govuk-!-margin-bottom-8"><%= t('presenter.questions.multiupload.none') %></p>
42
+ <% else %>
43
+ <p class="govuk-!-margin-bottom-8"><%= component.validation['max_files'].to_i > 1 ? t('presenter.questions.multiupload.plural', num: component.validation['max_files']) : t('presenter.questions.multiupload.single_upload') %></p>
44
+ <% end %>
45
+ <% else %>
46
+ <p class="govuk-!-margin-bottom-8"><%= multiupload_files_remaining %></p>
47
+ <% end %>
48
+
49
+ <% if answered?(component.id) && uploads_remaining.positive? && @page_answers.send(component.id)[component.id].compact.count.positive? %>
50
+ <button class="govuk-button govuk-!-margin-bottom-8 govuk-button--secondary" data-multiupload-element="add-another-file"><%= t('presenter.questions.multiupload.add_another') %></button>
51
+ <% end %>
@@ -37,7 +37,9 @@
37
37
 
38
38
  <div class="govuk-grid-row">
39
39
  <div class="govuk-grid-column-two-thirds">
40
+ <% unless @page.body == default_text('body') %>
40
41
  <%= render 'metadata_presenter/attribute/body' %>
42
+ <% end %>
41
43
 
42
44
  <%= render partial: 'metadata_presenter/component/components',
43
45
  locals: {
@@ -12,7 +12,10 @@
12
12
  </h1>
13
13
 
14
14
  <%= render 'metadata_presenter/attribute/lede' %>
15
- <%= render 'metadata_presenter/attribute/body' %>
15
+
16
+ <% unless @page.body == default_text('body') %>
17
+ <%= render 'metadata_presenter/attribute/body' %>
18
+ <% end %>
16
19
 
17
20
  <%= form_for @page_answers, as: :answers, url: @page.url, method: :post do |f| %>
18
21
 
@@ -21,7 +21,7 @@ Rails.application.config.supported_components =
21
21
  content: %w(content)
22
22
  },
23
23
  singlequestion: {
24
- input: %w(text textarea number date radios checkboxes email upload autocomplete),
24
+ input: %w(text textarea number date radios checkboxes email upload multiupload autocomplete),
25
25
  content: %w()
26
26
  }
27
27
  })
@@ -41,6 +41,18 @@ en:
41
41
  payment_enabled: You still need to pay
42
42
  continue_to_pay_button: Continue to pay
43
43
  application_complete: 'Application complete'
44
+ questions:
45
+ multiupload:
46
+ remove_file: 'Delete'
47
+ add_another: 'Add another file'
48
+ single_upload: 'Maximum file size 7MB'
49
+ answered_singular: 'You can add 1 more file (maximum 7MB per file)'
50
+ answered_plural: 'You can add %{num} more files (maximum 7MB per file)'
51
+ answered_count_singular: 'You have added 1 file'
52
+ answered_count_plural: 'You have added %{num} files'
53
+ singular: 'You can add 1 more file (maximum 7MB)'
54
+ plural: 'You can add up to %{num} files (maximum 7MB per file)'
55
+ none: "You can't add any more files. Remove a file if you need to add a different one."
44
56
  save_and_return:
45
57
  save: 'Save for later'
46
58
  show:
data/config/routes.rb CHANGED
@@ -7,6 +7,7 @@ MetadataPresenter::Engine.routes.draw do
7
7
  # We are not adding rails ujs to the editor app so we need to make it
8
8
  # as get verb.
9
9
  get '/reserved/file/:component_id', to: 'file#destroy', as: :remove_file
10
+ get '/reserved/file/:component_id/:file_uuid', to: 'file#remove_multifile', as: :remove_multifile
10
11
 
11
12
  get 'session/expired', to: 'session#expired'
12
13
  get 'session/complete', to: 'session#complete'
@@ -0,0 +1,30 @@
1
+ {
2
+ "_id": "component.multiupload",
3
+ "_type": "multiupload",
4
+ "errors": {},
5
+ "legend": "Question",
6
+ "hint": "",
7
+ "name": "component-name",
8
+ "validation": {
9
+ "required": true,
10
+ "max_size": "7340032",
11
+ "virus_scan": true,
12
+ "max_files": "1",
13
+ "multiupload": true,
14
+ "accept": [
15
+ "text/csv",
16
+ "text/plain",
17
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
18
+ "application/msword",
19
+ "application/vnd.oasis.opendocument.spreadsheet",
20
+ "application/vnd.oasis.opendocument.text",
21
+ "application/pdf",
22
+ "application/rtf",
23
+ "application/csv",
24
+ "image/jpeg",
25
+ "image/png",
26
+ "application/vnd.ms-excel",
27
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
28
+ ]
29
+ }
30
+ }
@@ -4,7 +4,6 @@
4
4
  "section_heading": "",
5
5
  "heading": "Title",
6
6
  "lede": "",
7
- "body": "[Optional content]",
8
7
  "components": [],
9
8
  "url": ""
10
9
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_id": "error.max_files",
3
+ "_type": "string.error",
4
+ "description": "Input (number) is larger than the maximum allowed",
5
+ "value": "Your answer for \"%{control}\" must be %{maximum} or lower"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_id": "error.multiupload",
3
+ "_type": "string.error",
4
+ "description": "Cannot upload files with duplicate filenames",
5
+ "value": "The selected file cannot have the same name as a file you have already selected. Please check you aren't uploading the same file again."
6
+ }
@@ -0,0 +1 @@
1
+ { "max_files": "10" }
@@ -69,6 +69,12 @@
69
69
  }
70
70
  },
71
71
  "2ef7d11e-0307-49e9-9fe2-345dc528dd66": {
72
+ "_type": "flow.page",
73
+ "next": {
74
+ "default": "2ef7d11e-0307-49e9-9fe2-345dc528dd67"
75
+ }
76
+ },
77
+ "2ef7d11e-0307-49e9-9fe2-345dc528dd67": {
72
78
  "_type": "flow.page",
73
79
  "next": {
74
80
  "default": "c7755991-436b-4495-afa6-803db58cefbc"
@@ -574,6 +580,73 @@
574
580
  }
575
581
  ]
576
582
  },
583
+ {
584
+ "_id": "page.dog-picture-2",
585
+ "url": "dog-picture-2",
586
+ "_type": "page.singlequestion",
587
+ "_uuid": "2ef7d11e-0307-49e9-9fe2-345dc528dd67",
588
+ "heading": "Multiupload",
589
+ "components": [
590
+ {
591
+ "_id": "dog-picture_upload_2",
592
+ "name": "dog-picture_upload_2",
593
+ "_type": "multiupload",
594
+ "_uuid": "f056a76e-ec3f-47ae-b625-1bba92220ad2",
595
+ "hint": "",
596
+ "legend": "Upload your best dog photos",
597
+ "validation": {
598
+ "required": true,
599
+ "max_files": "3",
600
+ "multiupload": true,
601
+ "accept": [
602
+ "audio/*",
603
+ "image/bmp",
604
+ "text/csv",
605
+ "application/vnd.ms-excel",
606
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
607
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
608
+ "application/vnd.ms-excel.sheet.macroEnabled.12",
609
+ "application/vnd.ms-excel.template.macroEnabled.12",
610
+ "application/vnd.ms-excel.addin.macroEnabled.12",
611
+ "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
612
+ "image/gif",
613
+ "image/*",
614
+ "application/x-iwork-pages-sffpages",
615
+ "image/jpeg",
616
+ "application/pdf",
617
+ "text/plain",
618
+ "image/png",
619
+ "application/vnd.ms-powerpoint",
620
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
621
+ "application/vnd.openxmlformats-officedocument.presentationml.template",
622
+ "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
623
+ "application/vnd.ms-powerpoint.addin.macroEnabled.12",
624
+ "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
625
+ "application/vnd.ms-powerpoint.template.macroEnabled.12",
626
+ "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
627
+ "text/rtf",
628
+ "excel",
629
+ "csv",
630
+ "image/svg+xml",
631
+ "pdf",
632
+ "word",
633
+ "rtf",
634
+ "plaintext",
635
+ "image/tiff",
636
+ "video/*",
637
+ "application/msword",
638
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
639
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
640
+ "application/vnd.ms-word.document.macroEnabled.12",
641
+ "application/vnd.ms-word.template.macroEnabled.12",
642
+ "application/csv"
643
+ ],
644
+ "max_size": 7340032,
645
+ "virus_scan": true
646
+ }
647
+ }
648
+ ]
649
+ },
577
650
  {
578
651
  "_id": "page.countries",
579
652
  "url": "countries",
@@ -1,3 +1,3 @@
1
1
  module MetadataPresenter
2
- VERSION = '3.0.15'.freeze
2
+ VERSION = '3.2.0'.freeze
3
3
  end
@@ -17,6 +17,35 @@
17
17
  "type": "string",
18
18
  "content": true,
19
19
  "multiline": true
20
+ },
21
+ "conditionals": {
22
+ "$ref": "#/definitions/conditionals"
23
+ }
24
+ },
25
+ "definitions": {
26
+ "conditionals": {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "object",
30
+ "properties": {
31
+ "_uuid": {
32
+ "type": "string",
33
+ "title": "Unique identifier of the conditional",
34
+ "description": "Used internally in the editor and the runner"
35
+ },
36
+ "_type": {
37
+ "type": "string",
38
+ "enum": [
39
+ "if",
40
+ "and",
41
+ "or"
42
+ ]
43
+ },
44
+ "expressions": {
45
+ "$ref": "definition.expressions"
46
+ }
47
+ }
48
+ }
20
49
  }
21
50
  },
22
51
  "allOf": [
@@ -0,0 +1,42 @@
1
+ {
2
+ "$id": "http://gov.uk/schema/v1.0.0/multiupload",
3
+ "_name": "component.multiupload",
4
+ "title": "Multiupload",
5
+ "description": "Let users select and upload one or more files",
6
+ "type": "object",
7
+ "properties": {
8
+ "_type": {
9
+ "const": "multiupload"
10
+ },
11
+ "max_files": {
12
+ "title": "Maximum number of files",
13
+ "description": "Maximum number of files a user can upload",
14
+ "type": "number"
15
+ },
16
+ "min_files": {
17
+ "title": "Minimum number of files",
18
+ "description": "Minimum number of files a user can upload - 1 if required, 0 if not required",
19
+ "type": "number"
20
+ }
21
+ },
22
+ "allOf": [
23
+ {
24
+ "$ref": "definition.field"
25
+ },
26
+ {
27
+ "$ref": "definition.width_class.input"
28
+ },
29
+ {
30
+ "$ref": "validations#/definitions/errors_accept"
31
+ },
32
+ {
33
+ "$ref": "validations#/definitions/errors_max_size"
34
+ },
35
+ {
36
+ "$ref": "validations#/definitions/errors_virus_scan"
37
+ },
38
+ {
39
+ "$ref": "validations#/definitions/file_bundle"
40
+ }
41
+ ]
42
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$id": "http://gov.uk/schema/v1.0.0/definition/expression_item",
3
+ "_name": "definition.expression_item",
4
+ "title": "Expression item",
5
+ "description": "Component that provides an expression item",
6
+ "type": "object",
7
+ "properties": {
8
+ "operator": {
9
+ "type": "string",
10
+ "enum": [
11
+ "is",
12
+ "is_not",
13
+ "is_answered",
14
+ "is_not_answered"
15
+ ],
16
+ "page": {
17
+ "type": "string"
18
+ },
19
+ "component": {
20
+ "type": "string"
21
+ },
22
+ "field": {
23
+ "type": "string"
24
+ }
25
+ }
26
+ },
27
+ "required": [
28
+ "operator",
29
+ "page",
30
+ "component",
31
+ "field"
32
+ ],
33
+ "category": [
34
+ "expression_item"
35
+ ]
36
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$id": "http://gov.uk/schema/v1.0.0/definition/expressions",
3
+ "_name": "definition.expressions",
4
+ "title": "Expressions",
5
+ "type": "array",
6
+ "items": {
7
+ "$ref": "definition.expression_item"
8
+ },
9
+ "required": [
10
+ "items"
11
+ ],
12
+ "category": [
13
+ "expression"
14
+ ]
15
+ }
@@ -48,33 +48,7 @@
48
48
  "type": "string"
49
49
  },
50
50
  "expressions": {
51
- "$ref": "#/definitions/expressions"
52
- }
53
- }
54
- }
55
- },
56
- "expressions": {
57
- "type": "array",
58
- "items": {
59
- "type": "object",
60
- "properties": {
61
- "operator": {
62
- "type": "string",
63
- "enum": [
64
- "is",
65
- "is_not",
66
- "is_answered",
67
- "is_not_answered"
68
- ],
69
- "page": {
70
- "type": "string"
71
- },
72
- "component": {
73
- "type": "string"
74
- },
75
- "field": {
76
- "type": "string"
77
- }
51
+ "$ref": "definition.expressions"
78
52
  }
79
53
  }
80
54
  }
@@ -14,9 +14,6 @@
14
14
  "heading": {
15
15
  "multiline": true
16
16
  },
17
- "body": {
18
- "multiline": true
19
- },
20
17
  "lede": {
21
18
  "multiline": true
22
19
  },
@@ -118,6 +118,20 @@
118
118
  }
119
119
  ]
120
120
  },
121
+ "max_files": {
122
+ "title": "Maximum files",
123
+ "description": "The maximum number of fiels a user can upload",
124
+ "type": "number",
125
+ "minimum": 0
126
+ },
127
+ "errors_max_files": {
128
+ "title": "Error messages for 'Maximum files'",
129
+ "allOf": [
130
+ {
131
+ "$ref": "#/definitions/error_strings"
132
+ }
133
+ ]
134
+ },
121
135
  "pattern": {
122
136
  "title": "Pattern to match string against",
123
137
  "description": "A regular expression for validating users’ answers",
@@ -450,6 +464,9 @@
450
464
  },
451
465
  "exclusive_minimum": {
452
466
  "$ref": "#/definitions/exclusive_minimum"
467
+ },
468
+ "max_files": {
469
+ "$ref": "#/definitions/max_files"
453
470
  }
454
471
  }
455
472
  },
@@ -469,6 +486,9 @@
469
486
  },
470
487
  "exclusive_minimum": {
471
488
  "$ref": "#/definitions/errors_exclusive_minimum"
489
+ },
490
+ "max_files": {
491
+ "$ref": "#/definitions/max_files"
472
492
  }
473
493
  }
474
494
  }
@@ -509,6 +529,24 @@
509
529
  }
510
530
  }
511
531
  }
532
+ },
533
+ "file_bundle": {
534
+ "properties": {
535
+ "validation": {
536
+ "properties": {
537
+ "max_files": {
538
+ "$ref": "#/definitions/max_files"
539
+ }
540
+ }
541
+ },
542
+ "errors": {
543
+ "properties": {
544
+ "max_files": {
545
+ "$ref": "#/definitions/max_files"
546
+ }
547
+ }
548
+ }
549
+ }
512
550
  }
513
551
  },
514
552
  "category": [
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metadata_presenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.15
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MoJ Forms
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-14 00:00:00.000000000 Z
11
+ date: 2023-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: govuk_design_system_formbuilder
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: hashie
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
181
195
  - !ruby/object:Gem::Dependency
182
196
  name: rspec-rails
183
197
  requirement: !ruby/object:Gem::Requirement
@@ -332,6 +346,7 @@ files:
332
346
  - app/models/metadata_presenter/meta.rb
333
347
  - app/models/metadata_presenter/meta_item.rb
334
348
  - app/models/metadata_presenter/metadata.rb
349
+ - app/models/metadata_presenter/multi_upload_answer.rb
335
350
  - app/models/metadata_presenter/next_page.rb
336
351
  - app/models/metadata_presenter/offline_upload_adapter.rb
337
352
  - app/models/metadata_presenter/page.rb
@@ -362,6 +377,7 @@ files:
362
377
  - app/validators/metadata_presenter/date_before_validator.rb
363
378
  - app/validators/metadata_presenter/date_validator.rb
364
379
  - app/validators/metadata_presenter/email_validator.rb
380
+ - app/validators/metadata_presenter/max_files_validator.rb
365
381
  - app/validators/metadata_presenter/max_length_validator.rb
366
382
  - app/validators/metadata_presenter/max_size_validator.rb
367
383
  - app/validators/metadata_presenter/max_word_validator.rb
@@ -369,6 +385,7 @@ files:
369
385
  - app/validators/metadata_presenter/min_length_validator.rb
370
386
  - app/validators/metadata_presenter/min_word_validator.rb
371
387
  - app/validators/metadata_presenter/minimum_validator.rb
388
+ - app/validators/metadata_presenter/multiupload_validator.rb
372
389
  - app/validators/metadata_presenter/number_validator.rb
373
390
  - app/validators/metadata_presenter/required_validator.rb
374
391
  - app/validators/metadata_presenter/upload_validator.rb
@@ -400,6 +417,7 @@ files:
400
417
  - app/views/metadata_presenter/component/_content.html.erb
401
418
  - app/views/metadata_presenter/component/_date.html.erb
402
419
  - app/views/metadata_presenter/component/_email.html.erb
420
+ - app/views/metadata_presenter/component/_multiupload.html.erb
403
421
  - app/views/metadata_presenter/component/_number.html.erb
404
422
  - app/views/metadata_presenter/component/_radios.html.erb
405
423
  - app/views/metadata_presenter/component/_text.html.erb
@@ -445,6 +463,7 @@ files:
445
463
  - default_metadata/component/content.json
446
464
  - default_metadata/component/date.json
447
465
  - default_metadata/component/email.json
466
+ - default_metadata/component/multiupload.json
448
467
  - default_metadata/component/number.json
449
468
  - default_metadata/component/radios.json
450
469
  - default_metadata/component/text.json
@@ -473,6 +492,7 @@ files:
473
492
  - default_metadata/string/error.date_after.json
474
493
  - default_metadata/string/error.date_before.json
475
494
  - default_metadata/string/error.email.json
495
+ - default_metadata/string/error.max_files.json
476
496
  - default_metadata/string/error.max_length.json
477
497
  - default_metadata/string/error.max_size.json
478
498
  - default_metadata/string/error.max_word.json
@@ -480,11 +500,13 @@ files:
480
500
  - default_metadata/string/error.min_length.json
481
501
  - default_metadata/string/error.min_word.json
482
502
  - default_metadata/string/error.minimum.json
503
+ - default_metadata/string/error.multiupload.json
483
504
  - default_metadata/string/error.number.json
484
505
  - default_metadata/string/error.required.json
485
506
  - default_metadata/string/error.virus_scan.json
486
507
  - default_metadata/validations/date_after.json
487
508
  - default_metadata/validations/date_before.json
509
+ - default_metadata/validations/max_files.json
488
510
  - default_metadata/validations/max_length.json
489
511
  - default_metadata/validations/max_word.json
490
512
  - default_metadata/validations/maximum.json
@@ -531,6 +553,7 @@ files:
531
553
  - schemas/component/content.json
532
554
  - schemas/component/date.json
533
555
  - schemas/component/email.json
556
+ - schemas/component/multiupload.json
534
557
  - schemas/component/number.json
535
558
  - schemas/component/radios.json
536
559
  - schemas/component/text.json
@@ -557,6 +580,8 @@ files:
557
580
  - schemas/definition/conditions.exactly.json
558
581
  - schemas/definition/control.json
559
582
  - schemas/definition/data.json
583
+ - schemas/definition/expression_item.json
584
+ - schemas/definition/expressions.json
560
585
  - schemas/definition/field.json
561
586
  - schemas/definition/fieldset.json
562
587
  - schemas/definition/grouping.json