metadata_presenter 3.0.15 → 3.2.0

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