- <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' } do |f| %>
+ <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:bcc_completed).new(@template.preferences['bcc_completed']) do |ff| %>
diff --git a/config/routes.rb b/config/routes.rb
index d24756af..de403274 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -97,6 +97,7 @@ Rails.application.routes.draw do
resource :form, only: %i[show], controller: 'templates_form_preview'
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
resource :preferences, only: %i[show create], controller: 'templates_preferences'
+ resources :recipients, only: %i[create], controller: 'templates_recipients'
resources :submissions_export, only: %i[index new]
end
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'
@@ -127,6 +128,7 @@ Rails.application.routes.draw do
resources :values, only: %i[index], controller: 'submit_form_values'
resources :download, only: %i[index], controller: 'submit_form_download'
resources :decline, only: %i[create], controller: 'submit_form_decline'
+ resources :invite, only: %i[create], controller: 'submit_form_invite'
get :completed
end
diff --git a/lib/accounts.rb b/lib/accounts.rb
index 0ce789d7..ea754ce9 100644
--- a/lib/accounts.rb
+++ b/lib/accounts.rb
@@ -111,6 +111,8 @@ module Accounts
data
else
+ return Docuseal.default_pkcs if Docuseal::CERTS.present?
+
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value ||
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
end
@@ -143,6 +145,8 @@ module Accounts
value = EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
Docuseal::CERTS.merge(value)
+ elsif Docuseal::CERTS.present?
+ Docuseal::CERTS
else
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
end
diff --git a/lib/download_utils.rb b/lib/download_utils.rb
new file mode 100644
index 00000000..3fc32950
--- /dev/null
+++ b/lib/download_utils.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module DownloadUtils
+ LOCALHOSTS = %w[0.0.0.0 127.0.0.1 localhost].freeze
+
+ UnableToDownload = Class.new(StandardError)
+
+ module_function
+
+ def call(url)
+ uri = Addressable::URI.parse(url)
+
+ if Docuseal.multitenant?
+ raise UnableToDownload, "Error loading: #{uri.display_uri}. Only HTTPS is allowed." if uri.scheme != 'https'
+
+ if uri.host.in?(LOCALHOSTS)
+ raise UnableToDownload, "Error loading: #{uri.display_uri}. Can't download from localhost."
+ end
+ end
+
+ resp = conn.get(uri.display_uri.to_s)
+
+ raise UnableToDownload, "Error loading: #{uri.display_uri}" if resp.status >= 400
+
+ resp
+ end
+
+ def conn
+ Faraday.new do |faraday|
+ faraday.response :follow_redirects
+ end
+ end
+end
diff --git a/lib/submission_events.rb b/lib/submission_events.rb
index 1be9f953..e4f93e9e 100644
--- a/lib/submission_events.rb
+++ b/lib/submission_events.rb
@@ -14,6 +14,7 @@ module SubmissionEvents
phone_verified: 'Phone verified',
start_form: 'Submission started',
view_form: 'Form viewed',
+ invite_party: 'Invited',
complete_form: 'Submission completed',
api_complete_form: 'Submission completed via API'
}.freeze
diff --git a/lib/submissions.rb b/lib/submissions.rb
index 2fc9d386..878281b0 100644
--- a/lib/submissions.rb
+++ b/lib/submissions.rb
@@ -110,6 +110,7 @@ module Submissions
return email.downcase if email.to_s.include?('.om')
return email.downcase if email.to_s.include?('.mm')
return email.downcase if email.to_s.include?('.cm')
+ return email.downcase if email.to_s.include?('.et')
return email.downcase unless email.to_s.include?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<'))
diff --git a/lib/submissions/assign_defined_submitters.rb b/lib/submissions/assign_defined_submitters.rb
new file mode 100644
index 00000000..edbd26e5
--- /dev/null
+++ b/lib/submissions/assign_defined_submitters.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Submissions
+ module AssignDefinedSubmitters
+ module_function
+
+ def call(submission)
+ submission.submitters_order = 'preserved'
+
+ assign_defined_submitters(submission)
+ assign_linked_submitters(submission)
+
+ submission
+ end
+
+ def assign_defined_submitters(submission)
+ submission.template.submitters.to_a.select do |item|
+ next if item['email'].blank? && item['is_requester'].blank?
+ next if submission.submitters.any? { |e| e.uuid == item['uuid'] }
+
+ submission.submitters.new(
+ account_id: submission.account_id,
+ uuid: item['uuid'],
+ email: item['is_requester'] ? submission.template.author.email : item['email']
+ )
+ end
+ end
+
+ def assign_linked_submitters(submission)
+ submission.template.submitters.to_a.select do |item|
+ next if item['linked_to_uuid'].blank?
+ next if submission.submitters.any? { |e| e.uuid == item['uuid'] }
+
+ email = submission.submitters.find { |s| s.uuid == item['linked_to_uuid'] }&.email
+
+ next unless email
+
+ submission.submitters.new(
+ account_id: submission.account_id,
+ uuid: item['uuid'],
+ email:
+ )
+ end
+ end
+ end
+end
diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb
index 1eae90e2..5e0ab509 100644
--- a/lib/submissions/create_from_submitters.rb
+++ b/lib/submissions/create_from_submitters.rb
@@ -39,10 +39,21 @@ module Submissions
next if submission.submitters.blank?
+ maybe_add_invite_submitters(submission, template)
+
submission.tap(&:save!)
end
end
+ def maybe_add_invite_submitters(submission, template)
+ template.submitters.each do |item|
+ next if item['invite_by_uuid'].blank? ||
+ submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] }
+
+ submission.template_submitters << item
+ end
+ end
+
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil)
template_fields = (submission.template_fields || submission.template.fields).deep_dup
@@ -120,7 +131,6 @@ module Submissions
field['title'] = attrs['title'] if attrs['title'].present?
field['description'] = attrs['description'] if attrs['description'].present?
field['readonly'] = attrs['readonly'] if attrs.key?('readonly')
- field['redacted'] = attrs['redacted'] if attrs.key?('redacted')
field['required'] = attrs['required'] if attrs.key?('required')
if attrs.key?('default_value') && !field['type'].in?(%w[signature image initials file])
diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb
index 70aba66a..cd19e01d 100644
--- a/lib/submissions/generate_audit_trail.rb
+++ b/lib/submissions/generate_audit_trail.rb
@@ -348,13 +348,21 @@ module Submissions
events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event|
submitter = submission.submitters.find { |e| e.id == event.submitter_id }
+
+ text = SubmissionEvents::EVENT_NAMES[event.event_type.to_sym]
+
+ if event.event_type == 'invite_party' &&
+ (invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
+ (name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name'))
+ text += ['', invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' ')
+ end
+
[
"#{I18n.l(event.event_timestamp.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
"#{TimeUtils.timezone_abbr(account.timezone, event.event_timestamp)}",
composer.document.layout.formatted_text_box(
[
- { text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
- font: [FONT_NAME, { variant: :bold }] },
+ { text:, font: [FONT_NAME, { variant: :bold }] },
event.event_type.include?('send_') ? ' to ' : ' by ',
if event.event_type.include?('sms') || event.event_type.include?('phone')
event.data['phone'] || submitter.phone
diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb
index 07504420..08428040 100644
--- a/lib/submissions/generate_result_attachments.rb
+++ b/lib/submissions/generate_result_attachments.rb
@@ -532,6 +532,8 @@ module Submissions
begin
pdf.acro_form.create_appearances(force: true) if pdf.acro_form && pdf.acro_form[:NeedAppearances]
pdf.acro_form&.flatten
+ rescue HexaPDF::MissingGlyphError
+ nil
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
end
diff --git a/lib/submissions/normalize_param_utils.rb b/lib/submissions/normalize_param_utils.rb
index 05e80a9f..07ded95c 100644
--- a/lib/submissions/normalize_param_utils.rb
+++ b/lib/submissions/normalize_param_utils.rb
@@ -48,13 +48,15 @@ module Submissions
submitters.each do |submitter|
submitter.values.each_value do |value|
- attachment = attachments_index[value]
+ Array.wrap(value).each do |v|
+ attachment = attachments_index[v]
- next unless attachment
+ next unless attachment
- attachment.record = submitter
+ attachment.record = submitter
- attachment.save!
+ attachment.save!
+ end
end
end
end
diff --git a/lib/submitters/normalize_values.rb b/lib/submitters/normalize_values.rb
index b14a4270..bed672e0 100644
--- a/lib/submitters/normalize_values.rb
+++ b/lib/submitters/normalize_values.rb
@@ -11,7 +11,6 @@ module Submitters
UnknownFieldName = Class.new(BaseError)
InvalidDefaultValue = Class.new(BaseError)
UnknownSubmitterName = Class.new(BaseError)
- UnableToDownload = Class.new(BaseError)
TRUE_VALUES = ['1', 'true', true, 'TRUE', 'True', 'yes', 'YES', 'Yes'].freeze
FALSE_VALUES = ['0', 'false', false, 'FALSE', 'False', 'no', 'NO', 'No'].freeze
@@ -137,7 +136,7 @@ module Submitters
elsif type.in?(%w[signature initials]) && value.length < 60
find_or_create_blob_from_text(account, value, type)
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
- Marcel::MimeType.for(data).include?('image')
+ Marcel::MimeType.for(data).exclude?('octet-stream')
find_or_create_blob_from_base64(account, data, type)
else
raise InvalidDefaultValue, "Invalid value, url, base64 or text < 60 chars is expected: #{value.first(200)}..."
@@ -185,12 +184,7 @@ module Submitters
return blob if blob
- uri = Addressable::URI.parse(url)
- resp = conn.get(uri.display_uri.to_s)
-
- raise UnableToDownload, "Error loading: #{uri.display_uri}" if resp.status >= 400
-
- data = resp.body
+ data = DownloadUtils.call(url).body
checksum = Digest::MD5.base64digest(data)
@@ -215,11 +209,5 @@ module Submitters
nil
end
-
- def conn
- Faraday.new do |faraday|
- faraday.response :follow_redirects
- end
- end
end
end
diff --git a/lib/submitters/serialize_for_webhook.rb b/lib/submitters/serialize_for_webhook.rb
index fc411ec2..f856a086 100644
--- a/lib/submitters/serialize_for_webhook.rb
+++ b/lib/submitters/serialize_for_webhook.rb
@@ -22,8 +22,12 @@ module Submitters
submitter_name = (submitter.submission.template_submitters ||
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
+ decline_reason =
+ submitter.declined_at? ? submitter.submission_events.find_by(event_type: :decline_form).data['reason'] : nil
+
submitter.as_json(SERIALIZE_PARAMS)
- .merge('role' => submitter_name,
+ .merge('decline_reason' => decline_reason,
+ 'role' => submitter_name,
'preferences' => submitter.preferences.except('default_values'),
'values' => values,
'documents' => documents,
@@ -79,7 +83,7 @@ module Submitters
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
- { name: field_name, uuid: field['uuid'], value: }
+ { name: field_name, uuid: field['uuid'], value:, readonly: field['readonly'] == true }
end
end
diff --git a/lib/templates.rb b/lib/templates.rb
index a93fd300..54ecd842 100644
--- a/lib/templates.rb
+++ b/lib/templates.rb
@@ -23,4 +23,11 @@ module Templates
templates.where(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
end
+
+ def filter_undefined_submitters(template)
+ template.submitters.to_a.select do |item|
+ item['invite_by_uuid'].blank? && item['linked_to_uuid'].blank? &&
+ item['is_requester'].blank? && item['email'].blank?
+ end
+ end
end
diff --git a/lib/templates/process_document.rb b/lib/templates/process_document.rb
index 7d9653fa..8bbd8b09 100644
--- a/lib/templates/process_document.rb
+++ b/lib/templates/process_document.rb
@@ -3,11 +3,13 @@
module Templates
module ProcessDocument
DPI = 200
- FORMAT = '.jpg'
+ FORMAT = '.png'
ATTACHMENT_NAME = 'preview_images'
PDF_CONTENT_TYPE = 'application/pdf'
- Q = ENV.fetch('PAGE_QUALITY', '35').to_i
+ CONCURRENCY = 2
+ Q = 95
+ JPEG_Q = ENV.fetch('PAGE_QUALITY', '35').to_i
MAX_WIDTH = 1400
MAX_NUMBER_OF_PAGES_PROCESSED = 15
MAX_FLATTEN_FILE_SIZE = 20.megabytes
@@ -39,7 +41,10 @@ module Templates
image = Vips::Image.new_from_buffer(data, '')
image = image.autorot.resize(MAX_WIDTH / image.width.to_f)
- io = StringIO.new(image.write_to_buffer(FORMAT, Q: Q, interlace: true))
+ bitdepth = 2**image.stats.to_a[1..3].pluck(2).uniq.size
+
+ io = StringIO.new(image.write_to_buffer(FORMAT, compression: 7, filter: 0, bitdepth:,
+ palette: true, Q: Q, dither: 0))
ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(
@@ -62,27 +67,49 @@ module Templates
attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['number_of_pages'] = number_of_pages
- attachment.save!
+ ApplicationRecord.no_touching do
+ attachment.save!
+ end
max_pages_to_process = data.size < GENERATE_PREVIEW_SIZE_LIMIT ? MAX_NUMBER_OF_PAGES_PROCESSED : 1
- (0..[number_of_pages - 1, max_pages_to_process].min).each do |page_number|
- page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number)
- page = page.resize(MAX_WIDTH / page.width.to_f)
+ pool = Concurrent::FixedThreadPool.new(CONCURRENCY)
- io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true))
+ promises =
+ (0..[number_of_pages - 1, max_pages_to_process].min).map do |page_number|
+ Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(data, page_number) }
+ end
+ Concurrent::Promise.zip(*promises).value!.each do |blob|
ApplicationRecord.no_touching do
ActiveStorage::Attachment.create!(
- blob: ActiveStorage::Blob.create_and_upload!(
- io:, filename: "#{page_number}#{FORMAT}",
- metadata: { analyzed: true, identified: true, width: page.width, height: page.height }
- ),
+ blob:,
name: ATTACHMENT_NAME,
record: attachment
)
end
end
+
+ pool.kill
+ end
+
+ def build_and_upload_blob(data, page_number)
+ page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number)
+ page = page.resize(MAX_WIDTH / page.width.to_f)
+
+ bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size
+
+ io = StringIO.new(page.write_to_buffer(FORMAT, compression: 7, filter: 0, bitdepth:,
+ palette: true, Q: Q, dither: 0))
+
+ blob = ActiveStorage::Blob.new(
+ filename: "#{page_number}#{FORMAT}",
+ metadata: { analyzed: true, identified: true, width: page.width, height: page.height }
+ )
+
+ blob.upload(io)
+
+ blob
end
def maybe_flatten_form(data, pdf)
@@ -97,11 +124,9 @@ module Templates
pdf.write(io, incremental: false, validate: false)
io.string
- rescue StandardError => e
+ rescue StandardError
raise if Rails.env.development?
- Rollbar.error(e) if defined?(Rollbar)
-
data
end
@@ -121,7 +146,7 @@ module Templates
io = StringIO.new
command = [
- 'pdftocairo', '-jpeg', '-jpegopt', "progressive=y,quality=#{Q},optimize=y",
+ 'pdftocairo', '-jpeg', '-jpegopt', "progressive=y,quality=#{JPEG_Q},optimize=y",
'-scale-to-x', MAX_WIDTH, '-scale-to-y', '-1',
'-r', DPI, '-f', page_number + 1, '-l', page_number + 1,
'-singlefile', Shellwords.escape(file_path), '-'
@@ -140,7 +165,7 @@ module Templates
ApplicationRecord.no_touching do
ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(
- io:, filename: "#{page_number}#{FORMAT}",
+ io:, filename: "#{page_number}.jpg",
metadata: { analyzed: true, identified: true, width: page.width, height: page.height }
),
name: ATTACHMENT_NAME,
diff --git a/lib/templates/serialize_for_api.rb b/lib/templates/serialize_for_api.rb
index 569f8dc2..3087035b 100644
--- a/lib/templates/serialize_for_api.rb
+++ b/lib/templates/serialize_for_api.rb
@@ -19,7 +19,7 @@ module Templates
preview_image_attachments ||=
ActiveStorage::Attachment.joins(:blob)
- .where(blob: { filename: '0.jpg' })
+ .where(blob: { filename: ['0.jpg', '0.png'] })
.where(record_id: schema_documents.map(&:id),
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
@@ -29,7 +29,7 @@ module Templates
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
first_page_blob = preview_image_attachments.find { |e| e.record_id == attachment.id }&.blob
- first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: '0.jpg' })&.blob
+ first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
{
id: attachment.id,
diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb
index 65d542b3..aa4951bf 100644
--- a/spec/requests/templates_spec.rb
+++ b/spec/requests/templates_spec.rb
@@ -130,7 +130,7 @@ describe 'Templates API', type: :request do
attachment = template.schema_documents.preload(:blob).find { |e| e.uuid == template_attachment_uuid }
first_page_blob =
ActiveStorage::Attachment.joins(:blob)
- .where(blob: { filename: '0.jpg' })
+ .where(blob: { filename: '0.png' })
.where(record_id: template.schema_documents.map(&:id),
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
diff --git a/spec/system/submission_preview_spec.rb b/spec/system/submission_preview_spec.rb
index 5449f63a..b231fcff 100644
--- a/spec/system/submission_preview_spec.rb
+++ b/spec/system/submission_preview_spec.rb
@@ -9,9 +9,10 @@ RSpec.describe 'Submission Preview' do
context 'when not submitted' do
let(:submission) { create(:submission, template:, created_by_user: user) }
- let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
before do
+ template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) }
+
sign_in(user)
visit submissions_preview_path(slug: submission.slug)