Merge pull request #39 from docusealco/wip

pull/381/head
Pete Matsyburka 1 year ago committed by GitHub
commit fed420cae1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,29 +1,29 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.2.1) actioncable (7.2.1.1)
actionpack (= 7.2.1) actionpack (= 7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.2.1) actionmailbox (7.2.1.1)
actionpack (= 7.2.1) actionpack (= 7.2.1.1)
activejob (= 7.2.1) activejob (= 7.2.1.1)
activerecord (= 7.2.1) activerecord (= 7.2.1.1)
activestorage (= 7.2.1) activestorage (= 7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (7.2.1) actionmailer (7.2.1.1)
actionpack (= 7.2.1) actionpack (= 7.2.1.1)
actionview (= 7.2.1) actionview (= 7.2.1.1)
activejob (= 7.2.1) activejob (= 7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.2.1) actionpack (7.2.1.1)
actionview (= 7.2.1) actionview (= 7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4, < 3.2) rack (>= 2.2.4, < 3.2)
@ -32,35 +32,35 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (7.2.1) actiontext (7.2.1.1)
actionpack (= 7.2.1) actionpack (= 7.2.1.1)
activerecord (= 7.2.1) activerecord (= 7.2.1.1)
activestorage (= 7.2.1) activestorage (= 7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.2.1) actionview (7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (7.2.1) activejob (7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.2.1) activemodel (7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
activerecord (7.2.1) activerecord (7.2.1.1)
activemodel (= 7.2.1) activemodel (= 7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.2.1) activestorage (7.2.1.1)
actionpack (= 7.2.1) actionpack (= 7.2.1.1)
activejob (= 7.2.1) activejob (= 7.2.1.1)
activerecord (= 7.2.1) activerecord (= 7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.2.1) activesupport (7.2.1.1)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.3.1)
@ -320,7 +320,7 @@ GEM
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.4) net-http-persistent (4.0.4)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.16) net-imap (0.4.17)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@ -372,7 +372,7 @@ GEM
puma (6.4.3) puma (6.4.3)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.7) rack (3.1.8)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.0.0)
@ -382,20 +382,20 @@ GEM
rackup (2.1.0) rackup (2.1.0)
rack (>= 3) rack (>= 3)
webrick (~> 1.8) webrick (~> 1.8)
rails (7.2.1) rails (7.2.1.1)
actioncable (= 7.2.1) actioncable (= 7.2.1.1)
actionmailbox (= 7.2.1) actionmailbox (= 7.2.1.1)
actionmailer (= 7.2.1) actionmailer (= 7.2.1.1)
actionpack (= 7.2.1) actionpack (= 7.2.1.1)
actiontext (= 7.2.1) actiontext (= 7.2.1.1)
actionview (= 7.2.1) actionview (= 7.2.1.1)
activejob (= 7.2.1) activejob (= 7.2.1.1)
activemodel (= 7.2.1) activemodel (= 7.2.1.1)
activerecord (= 7.2.1) activerecord (= 7.2.1.1)
activestorage (= 7.2.1) activestorage (= 7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.2.1) railties (= 7.2.1.1)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@ -410,9 +410,9 @@ GEM
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (7.2.1) railties (7.2.1.1)
actionpack (= 7.2.1) actionpack (= 7.2.1.1)
activesupport (= 7.2.1) activesupport (= 7.2.1.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@ -559,7 +559,7 @@ GEM
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.18) zeitwerk (2.7.0)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux

@ -1,6 +1,6 @@
# Reporting a Vulnerability # Reporting a Vulnerability
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP. If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
**We have a bug bounty program to reward security researchers.** **We have a bug bounty program to reward security researchers.**

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Api
class SubmissionDocumentsController < ApiBaseController
load_and_authorize_resource :submission
def index
documents =
if @submission.submitters.all?(&:completed_at?)
last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
end
last_submitter.documents_attachments
else
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
if @submission.preview_documents.present? &&
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
@submission.preview_documents
else
ApplicationRecord.no_touching do
@submission.preview_documents.each(&:destroy)
end
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
end
end
ActiveRecord::Associations::Preloader.new(
records: documents,
associations: [:blob]
).call
render json: {
id: @submission.id,
documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
end
}
end
end
end

@ -38,6 +38,10 @@ class ApplicationController < ActionController::Base
end end
def default_url_options def default_url_options
if request.domain == 'docuseal.com'
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
end
Docuseal.default_url_options Docuseal.default_url_options
end end

@ -405,7 +405,8 @@ export default {
} }
if (this.field.preferences?.font_size) { if (this.field.preferences?.font_size) {
style.fontSize = this.field.preferences.font_size + 'pt' style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)`
} }
return style return style

@ -45,6 +45,7 @@
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@keydown.enter="onEnter" @keydown.enter="onEnter"
@focus="$emit('focus')" @focus="$emit('focus')"
@paste="onPaste"
> >
</div> </div>
</div> </div>
@ -98,6 +99,25 @@ export default {
this.$emit('submit') this.$emit('submit')
} }
}, },
onPaste (e) {
e.preventDefault()
let pasteData = e.clipboardData.getData('text').trim()
if (pasteData.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
pasteData = pasteData.split('.').reverse().join('-')
}
const parsedDate = new Date(pasteData)
if (!isNaN(parsedDate)) {
const inputEl = this.$refs.input
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
}
},
setCurrentDate () { setCurrentDate () {
const inputEl = this.$refs.input const inputEl = this.$refs.input

@ -693,6 +693,7 @@ export default {
'de-DE': 'DD.MM.YYYY', 'de-DE': 'DD.MM.YYYY',
'fr-FR': 'DD/MM/YYYY', 'fr-FR': 'DD/MM/YYYY',
'it-IT': 'DD/MM/YYYY', 'it-IT': 'DD/MM/YYYY',
'en-GB': 'DD/MM/YYYY',
'es-ES': 'DD/MM/YYYY' 'es-ES': 'DD/MM/YYYY'
} }
}, },

@ -55,6 +55,8 @@ class Submission < ApplicationRecord
has_one_attached :audit_trail has_one_attached :audit_trail
has_one_attached :combined_document has_one_attached :combined_document
has_many_attached :preview_documents
has_many :template_schema_documents, has_many :template_schema_documents,
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) }, ->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
through: :template, source: :documents_attachments through: :template, source: :documents_attachments

@ -1,6 +1,6 @@
<div class="text-center"> <div class="text-center">
<div class="join"> <div class="join">
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>"> <a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' || request.host == 'docuseal.com' %>">
<%= svg_icon 'world', class: 'w-5 h-5' %> <%= svg_icon 'world', class: 'w-5 h-5' %>
Global Global
</a> </a>

@ -1,5 +1,5 @@
<% align = field.dig('preferences', 'align') %> <% align = field.dig('preferences', 'align') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: #{field['preferences']['font_size']}pt" if field.dig('preferences', 'font_size') %>"> <field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size']}pt); line-height: `clamp(6pt, 2.0vw, #{field['preferences']['font_size'].to_i + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %> <% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden"> <div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%"> <div class="flex-grow flex overflow-hidden" style="min-height: 50%">

@ -34,6 +34,7 @@ Rails.application.routes.draw do
resources :submitter_form_views, only: %i[create] resources :submitter_form_views, only: %i[create]
resources :submitters, only: %i[index show update] resources :submitters, only: %i[index show update]
resources :submissions, only: %i[index show create destroy] do resources :submissions, only: %i[index show create destroy] do
resources :documents, only: %i[index], controller: 'submission_documents'
collection do collection do
resources :init, only: %i[create], controller: 'submissions' resources :init, only: %i[create], controller: 'submissions'
resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails

@ -11,7 +11,7 @@ module Accounts
new_user.uuid = SecureRandom.uuid new_user.uuid = SecureRandom.uuid
new_user.account = new_account new_user.account = new_account
new_user.encrypted_password = SecureRandom.hex new_user.encrypted_password = SecureRandom.hex
new_user.email = "#{SecureRandom.hex}@docuseal.co" new_user.email = "#{SecureRandom.hex}@docuseal.com"
account.templates.each do |template| account.templates.each do |template|
new_template = template.dup new_template = template.dup

@ -16,7 +16,15 @@ module ActionMailerConfigsInterceptor
end end
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
message.from = ENV.fetch('SMTP_FROM') from = ENV.fetch('SMTP_FROM').to_s.split(',').sample
message.from = from
if from == 'DocuSeal <info@docuseal.com>'
message.body.instance_variable_set(
:@raw_source, message.body.raw_source.gsub('https://docuseal.co/', 'https://docuseal.com/')
)
end
return message return message
end end

@ -22,7 +22,7 @@ module ActionMailerEventsObserver
emailable_type:, emailable_type:,
event_type: :send, event_type: :send,
email:, email:,
data: { method: mail.delivery_method.class.name.underscore }, data: { from: mail.from, method: mail.delivery_method.class.name.underscore },
event_datetime: Time.current event_datetime: Time.current
) )
end end

@ -12,7 +12,7 @@ module Docuseal
TWITTER_URL = 'https://twitter.com/docusealco' TWITTER_URL = 'https://twitter.com/docusealco'
TWITTER_HANDLE = '@docusealco' TWITTER_HANDLE = '@docusealco'
CHATGPT_URL = 'https://chatgpt.com/g/g-9hg8AAw0r-docuseal' CHATGPT_URL = 'https://chatgpt.com/g/g-9hg8AAw0r-docuseal'
SUPPORT_EMAIL = 'support@docuseal.co' SUPPORT_EMAIL = 'support@docuseal.com'
HOST = ENV.fetch('HOST', 'localhost') HOST = ENV.fetch('HOST', 'localhost')
AATL_CERT_NAME = 'docuseal_aatl' AATL_CERT_NAME = 'docuseal_aatl'
CONSOLE_URL = if Rails.env.development? CONSOLE_URL = if Rails.env.development?

@ -2,6 +2,8 @@
module Params module Params
class BaseValidator class BaseValidator
EMAIL_REGEXP = /\A[a-z0-9][\.']?(?:(?:[a-z0-9_-]+[\.\+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[\.-])*[a-z0-9]+\.[a-z]{2,}\z/i
InvalidParameterError = Class.new(StandardError) InvalidParameterError = Class.new(StandardError)
def self.call(...) def self.call(...)
@ -65,6 +67,27 @@ module Params
raise_error(message || "#{key} must follow the #{regexp.source} format") raise_error(message || "#{key} must follow the #{regexp.source} format")
end end
def email_format(params, key, message: nil)
return if params.blank?
return if params[key].blank?
return if params[key].to_s.strip.split(/\s*[;,]\s*/).all? { |email| email.match?(EMAIL_REGEXP) }
if Rails.env.production?
Rollbar.error(message || "#{key} must follow the email format") if defined?(Rollbar)
else
raise_error(message || "#{key} must follow the email format")
end
end
def unique_value(params, key, message: nil)
return unless params.is_a?(Array)
return if params.none?
return if params.all? { |p| p[key].blank? }
return if params.pluck(key).compact_blank.uniq.size == params.pluck(key).compact_blank.size
raise_error(message || "#{key} must be unique")
end
def in_path(params, path = []) def in_path(params, path = [])
old_path = @current_path old_path = @current_path

@ -22,6 +22,7 @@ module Params
required(params, %i[emails email]) required(params, %i[emails email])
type(params, :emails, String) type(params, :emails, String)
email_format(params, :emails, message: 'emails are invalid')
boolean(params, :send_email) boolean(params, :send_email)
type(params, :message, Hash) type(params, :message, Hash)
@ -43,8 +44,8 @@ module Params
type(params, :completed_redirect_url, String) type(params, :completed_redirect_url, String)
type(params, :bcc_completed, String) type(params, :bcc_completed, String)
type(params, :reply_to, String) type(params, :reply_to, String)
format(params, :bcc_completed, /@/, message: 'bcc_completed email is invalid') email_format(params, :bcc_completed, message: 'bcc_completed email is invalid')
format(params, :reply_to, /@/, message: 'reply_to email is invalid') email_format(params, :reply_to, message: 'reply_to email is invalid')
type(params, :message, Hash) type(params, :message, Hash)
type(params, :submitters, Array) type(params, :submitters, Array)
@ -60,6 +61,7 @@ module Params
if params[:submitters].present? if params[:submitters].present?
in_path(params, :submitters) do |submitters_params| in_path(params, :submitters) do |submitters_params|
type(submitters_params, 0, Hash) type(submitters_params, 0, Hash)
unique_value(submitters_params, :role)
end end
end end
@ -74,8 +76,8 @@ module Params
type(submitter_params, :name, String) type(submitter_params, :name, String)
type(submitter_params, :reply_to, String) type(submitter_params, :reply_to, String)
type(submitter_params, :email, String) type(submitter_params, :email, String)
format(submitter_params, :email, /@/, message: 'email is invalid') email_format(submitter_params, :email, message: 'email is invalid')
format(submitter_params, :reply_to, /@/, message: 'reply_to email is invalid') email_format(submitter_params, :reply_to, message: 'reply_to email is invalid')
type(submitter_params, :phone, String) type(submitter_params, :phone, String)
format(submitter_params, :phone, /\A\+\d+\z/, format(submitter_params, :phone, /\A\+\d+\z/,
message: 'phone should start with +<country code> and contain only digits') message: 'phone should start with +<country code> and contain only digits')
@ -105,7 +107,7 @@ module Params
type(params, :order, String) type(params, :order, String)
type(params, :completed_redirect_url, String) type(params, :completed_redirect_url, String)
type(params, :bcc_completed, String) type(params, :bcc_completed, String)
format(params, :bcc_completed, /@/, message: 'bcc_completed email is invalid') email_format(params, :bcc_completed, message: 'bcc_completed email is invalid')
type(params, :message, Hash) type(params, :message, Hash)
in_path(params, :message) do |message_params| in_path(params, :message) do |message_params|

@ -0,0 +1,96 @@
# frozen_string_literal: true
module Submissions
module GeneratePreviewAttachments
module_function
# rubocop:disable Metrics
def call(submission, values_hash: nil)
values_hash ||= build_values_hash(submission)
with_signature_id = submission.account.account_configs
.exists?(key: AccountConfig::WITH_SIGNATURE_ID, value: true)
is_flatten =
submission.account.account_configs
.find_or_initialize_by(key: AccountConfig::FLATTEN_RESULT_PDF_KEY).value != false
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten)
submission.submitters.where(completed_at: nil).preload(attachments_attachments: :blob).each do |submitter|
GenerateResultAttachments.fill_submitter_fields(submitter, submission.account, pdfs_index,
with_signature_id:, is_flatten:)
end
template = submission.template
image_pdfs = []
original_documents = template.documents.preload(:blob)
result_attachments =
(submission.template_schema || template.schema).map do |item|
pdf = pdfs_index[item['attachment_uuid']]
if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
pdf = GenerateResultAttachments.normalize_image_pdf(pdf)
image_pdfs << pdf
end
build_pdf_attachment(pdf:, submission:,
uuid: item['attachment_uuid'],
values_hash:,
name: item['name'])
end
return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
images_pdf =
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
pdf.pages.each { |page| doc.pages << doc.import(page) }
end
images_pdf = GenerateResultAttachments.normalize_image_pdf(images_pdf)
images_pdf_attachment =
build_pdf_attachment(
pdf: images_pdf,
submission:,
uuid: GenerateResultAttachments.images_pdf_uuid(original_documents.select(&:image?)),
values_hash:,
name: template.name
)
ApplicationRecord.no_touching do
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
end
end
def build_values_hash(submission)
submission.submitters.reduce({}) { |acc, s| acc.merge(s.values) }.hash
end
def build_pdf_attachment(pdf:, submission:, uuid:, name:, values_hash:)
io = StringIO.new
begin
pdf.write(io, incremental: true, validate: false)
rescue HexaPDF::MalformedPDFError => e
Rollbar.error(e) if defined?(Rollbar)
pdf.write(io, incremental: false, validate: false)
end
ActiveStorage::Attachment.new(
blob: ActiveStorage::Blob.create_and_upload!(io: io.tap(&:rewind), filename: "#{name}.pdf"),
metadata: { original_uuid: uuid,
values_hash:,
analyzed: true,
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
name: 'preview_documents',
record: submission
)
end
# rubocop:enable Metrics
end
end

@ -92,19 +92,13 @@ module Submissions
end end
def generate_pdfs(submitter) def generate_pdfs(submitter)
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center) configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID])
with_signature_id = submitter.account.account_configs
.exists?(key: AccountConfig::WITH_SIGNATURE_ID, value: true)
is_flatten =
submitter.account.account_configs
.find_or_initialize_by(key: AccountConfig::FLATTEN_RESULT_PDF_KEY).value != false
account = submitter.account with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
attachments_data_cache = {} is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
pdfs_index = build_pdfs_index(submitter, flatten: is_flatten) pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten)
if with_signature_id || submitter.account.testing? if with_signature_id || submitter.account.testing?
pdfs_index.each_value do |pdf| pdfs_index.each_value do |pdf|
@ -145,6 +139,16 @@ module Submissions
end end
end end
fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:)
end
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:)
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
attachments_data_cache = {}
return pdfs_index if submitter.submission.template_fields.blank?
submitter.submission.template_fields.each do |field| submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid next if field['submitter_uuid'] != submitter.uuid
@ -514,13 +518,13 @@ module Submissions
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':')) Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':'))
end end
def build_pdfs_index(submitter, flatten: true) def build_pdfs_index(submission, submitter: nil, flatten: true)
latest_submitter = find_last_submitter(submitter) latest_submitter = find_last_submitter(submission, submitter:)
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
documents = latest_submitter&.documents&.preload(:blob).to_a.presence documents = latest_submitter&.documents&.preload(:blob).to_a.presence
documents ||= submitter.submission.template_schema_documents.preload(:blob) documents ||= submission.template_schema_documents.preload(:blob)
documents.to_h do |attachment| documents.to_h do |attachment|
pdf = pdf =
@ -582,11 +586,11 @@ module Submissions
font_wrapper.custom_glyph(replace_with, character) font_wrapper.custom_glyph(replace_with, character)
end end
def find_last_submitter(submitter) def find_last_submitter(submission, submitter: nil)
submitter.submission.submitters submission.submitters
.select(&:completed_at?) .select(&:completed_at?)
.select { |e| e.id != submitter.id && e.completed_at <= submitter.completed_at } .select { |e| submitter.nil? ? true : e.id != submitter.id && e.completed_at <= submitter.completed_at }
.max_by(&:completed_at) .max_by(&:completed_at)
end end
def build_pdf_from_image(attachment) def build_pdf_from_image(attachment)

@ -7,7 +7,11 @@ FactoryBot.define do
author factory: %i[user] author factory: %i[user]
name { Faker::Book.title } name { Faker::Book.title }
after(:create) do |template| transient do
submitter_count { 1 }
end
after(:create) do |template, evaluator|
blob = ActiveStorage::Blob.create_and_upload!( blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/fixtures/sample-document.pdf').open, io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
filename: 'sample-document.pdf', filename: 'sample-document.pdf',
@ -22,39 +26,46 @@ FactoryBot.define do
Templates::ProcessDocument.call(attachment, attachment.download) Templates::ProcessDocument.call(attachment, attachment.download)
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }] template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
template.submitters = [ number_words = %w[first second third fourth fifth sixth seventh eighth ninth tenth]
{
'name' => 'First Party', template.submitters = Array.new(evaluator.submitter_count) do |i|
'uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c'
}
]
template.fields = [
{
'uuid' => '21637fc9-0655-45df-8952-04ec64949e85',
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
'name' => 'First Name',
'type' => 'text',
'required' => true,
'areas' => [
{
'x' => 0.09027777777777778,
'y' => 0.1197252208047105,
'w' => 0.3069444444444444,
'h' => 0.03336604514229637,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{ {
'uuid' => '1f97f8e3-dc82-4586-aeea-6ebed6204e46', 'name' => "#{number_words[i]&.capitalize} Party",
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c', 'uuid' => SecureRandom.uuid
'name' => '',
'type' => 'signature',
'required' => true,
'areas' => []
} }
] end
template.fields = template.submitters.reduce([]) do |fields, submitter|
fields += [
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'First Name',
'type' => 'text',
'required' => true,
'areas' => [
{
'x' => 0.09027777777777778,
'y' => 0.1197252208047105,
'w' => 0.3069444444444444,
'h' => 0.03336604514229637,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => '',
'type' => 'signature',
'required' => true,
'areas' => []
}
]
fields
end
template.save! template.save!
end end

@ -0,0 +1,139 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Params::BaseValidator do
let(:validator) { described_class.new({}) }
describe '#email_format' do
it 'when email is valid' do
emails = [
' john.doe@example.com ',
'john.doe@example.com',
'jane+newsletter@domain.org',
'mike_smith@company.net',
'lisa-wong@sub.example.co.uk',
'peter@webmail.com',
'anna.jones123@my-domain.com',
'contact@company.email',
'info@my-company123.org',
'hello.world@business.info',
'feedback@new-domain.com',
'alerts+user@localdomain.net',
'webmaster@industry.biz',
'services@agency.example',
'george123@consultant.pro',
'sales-team@company.io'
]
emails.each do |email|
expect { validator.email_format({ email: }, :email) }.not_to raise_error
end
end
it 'when signle email is invalid' do
emails = [
'jone.doe@',
'mike.smith@',
'jane.doe@@example.com',
'@example.com',
'lisa.wong@example',
'peter.parker..@example.com',
'anna.jones@.com',
'jack.brown@com',
'john doe@example.com',
'laura.martin@ example.com',
'dave.clark@example .com',
'susan.green@example,com',
'chris.lee@example;com',
'jenny.king@.example.com',
'.henry.ford@example.com',
'amy.baker@sub_domain.com',
'george.morris@-example.com',
'nancy.davis@example..com',
'kevin.white@.',
'diana.robinson@.example..com',
'oliver.scott@example.c',
'email1@g.comemail@g.com',
'user.name@subdomain.example@example.com',
'double@at@sign.com',
'user@@example.com',
'email@123.123.123.123',
'this...is@strange.but.valid.com',
'mix-and.match@strangely-formed-email_address.com',
'email@domain..com',
'user@-weird-domain-.com',
'user.name@[IPv6:2001:db8::1]',
'tricky.email@sub.example-.com',
'user@domain.c0m'
]
emails.each do |email|
expect do
validator.email_format({ email: }, :email)
end.to raise_error(described_class::InvalidParameterError, 'email must follow the email format')
end
end
it 'when multiple emails are valid' do
emails = [
'john.doe@example.com, jane.doe+newsletter@domain.org',
'joshua@automobile.car ; chloe+fashion@food.delivery',
'mike-smith@company.net;lisa.wong-sales@sub.example.co.uk',
'peter.parker+info@webmail.com,laura.martin-office@company.co',
'anna.jones123@my-domain.com, jack.brown+work@college.edu',
'susan.green@business-info.org; dave.clark+personal@nonprofit.org',
'chris.lee+team@new-domain.com;jenny.king.marketing@localdomain.net',
'george.morris@consultant.pro; nancy.davis-office@company.io',
'joshua-jones@automobile.car; chloe.taylor+fashion@food.delivery',
'ryan.moore+alerts@music-band.com,isabella.walker.design@fashion.design',
'support-team@company.com, contact.us@domain.org',
'admin.office@industry.biz, hr.department@service.pro',
'feedback@agency-example.org; hello.world@creative-studio.net',
'sales-team@e-commerce.shop, support.department@technology.co',
'media.contact@financial.servicesl; events-coordinator@food.delivery',
'order@music-band.com; info.support@creative.example',
'design.team@webmail.com , admin-office@company.co',
'contact.sales@sub-example.co.uk, support+info@legal.gov',
'support@media.group;subscribe-updates@concert.events'
]
emails.each do |email|
expect { validator.email_format({ email: }, :email) }.not_to raise_error
end
end
it 'when multiple emails are invalid' do
emails = [
'jone@gmail.com, ,mike@gmail.com',
'john.doe@example.com dave@nonprofit.org',
'; oliver.scott@example.com',
'amy.baker@ example.com, george.morris@ example.com',
'jenny.king@example.com . diana.robinson@example.com',
'nancy.davis@.com, henry.ford@.com',
'jack.brown@example.com, laura.martin@example .com',
'anna.jones@example,com lisa.wong@example.com',
'dave.clark@example.com kevin.white@example;com',
'susan.green@ example.com; john.doe@example.com',
'amy.baker@sub_domain.com george.morris@-example.com',
'nancy.davis@example..com john.doe@example.c',
'peter.parker@example.com, .henry.ford@example.com',
'diana.robinson@.example..com, mike.smith@.',
'oliver.scott@example.com; laura.martin@ example.com, jane.doe@@example.com'
]
emails.each do |email|
expect do
validator.email_format({ email: }, :email)
end.to raise_error(described_class::InvalidParameterError, 'email must follow the email format')
end
end
it 'when email is invalid with custom message' do
expect do
validator.email_format({ email: 'jone.doe@' }, :email, message: 'email is invalid')
end.to raise_error(described_class::InvalidParameterError, 'email is invalid')
end
end
end

@ -7,6 +7,7 @@ describe 'Submission API', type: :request do
let!(:author) { create(:user, account:) } let!(:author) { create(:user, account:) }
let!(:folder) { create(:template_folder, account:) } let!(:folder) { create(:template_folder, account:) }
let!(:templates) { create_list(:template, 2, account:, author:, folder:) } let!(:templates) { create_list(:template, 2, account:, author:, folder:) }
let!(:multiple_submitters_template) { create(:template, submitter_count: 3, account:, author:, folder:) }
describe 'GET /api/submissions' do describe 'GET /api/submissions' do
it 'returns a list of submissions' do it 'returns a list of submissions' do
@ -56,20 +57,98 @@ describe 'Submission API', type: :request do
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json)) expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
end end
it 'creates a submission when some submitter roles are not provided' do
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
template_id: multiple_submitters_template.id,
send_email: true,
submitters: [
{ role: 'First Role', email: 'john.doe@example.com' },
{ email: 'jane.doe@example.com' },
{ email: 'mike.doe@example.com' }
]
}.to_json
expect(response).to have_http_status(:ok)
submission = Submission.last
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
expect(response.parsed_body[0]['role']).to eq('First Party')
expect(response.parsed_body[0]['email']).to eq('john.doe@example.com')
expect(response.parsed_body[1]['role']).to eq('Second Party')
expect(response.parsed_body[1]['email']).to eq('jane.doe@example.com')
expect(response.parsed_body[2]['role']).to eq('Third Party')
expect(response.parsed_body[2]['email']).to eq('mike.doe@example.com')
end
it 'returns an error if the submitter email is invalid' do
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
template_id: templates[0].id,
send_email: true,
submitters: [
{ role: 'First Role', email: 'john@example' }
]
}.to_json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body).to eq({ 'error' => 'email is invalid in `submitters[0]`.' })
end
it 'returns an error if the template fields are missing' do
templates[0].update(fields: [])
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
template_id: templates[0].id,
send_email: true,
submitters: [{ role: 'First Role', email: 'john.doe@example.com' }]
}.to_json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body).to eq({ 'error' => 'Template does not contain fields' })
end
it 'returns an error if submitter roles are not unique' do
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
template_id: multiple_submitters_template.id,
send_email: true,
submitters: [
{ role: 'First Role', email: 'john.doe@example.com' },
{ role: 'First Role', email: 'jane.doe@example.com' }
]
}.to_json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body).to eq({ 'error' => 'role must be unique in `submitters`.' })
end
end end
describe 'POST /api/submissions/emails' do describe 'POST /api/submissions/emails' do
it 'creates a submission using email' do it 'creates a submission using email' do
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { post '/api/submissions/emails', headers: { 'x-auth-token': author.access_token.token }, params: {
template_id: templates[0].id, template_id: templates[0].id,
emails: 'john.doe@example.com' emails: 'john.doe@example.com,jane.doe@example.com'
}.to_json }.to_json
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
submission = Submission.last submissions = Submission.last(2)
submissions_body = submissions.reduce([]) { |acc, submission| acc + create_submission_body(submission) }
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json)) expect(response.parsed_body).to eq(JSON.parse(submissions_body.to_json))
end
it 'returns an error if emails are invalid' do
post '/api/submissions/emails', headers: { 'x-auth-token': author.access_token.token }, params: {
template_id: templates[0].id,
emails: 'amy.baker@example.com, george.morris@example.com@gmail.com'
}.to_json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body).to eq({ 'error' => 'emails are invalid' })
end end
end end

@ -144,8 +144,8 @@ describe 'Templates API', type: :request do
name: template.name, name: template.name,
fields: [ fields: [
{ {
'uuid' => '21637fc9-0655-45df-8952-04ec64949e85', 'uuid' => template.fields[0]['uuid'],
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c', 'submitter_uuid' => template.submitters[0]['uuid'],
'name' => 'First Name', 'name' => 'First Name',
'type' => 'text', 'type' => 'text',
'required' => true, 'required' => true,
@ -161,8 +161,8 @@ describe 'Templates API', type: :request do
] ]
}, },
{ {
'uuid' => '1f97f8e3-dc82-4586-aeea-6ebed6204e46', 'uuid' => template.fields[1]['uuid'],
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c', 'submitter_uuid' => template.submitters[0]['uuid'],
'name' => '', 'name' => '',
'type' => 'signature', 'type' => 'signature',
'required' => true, 'required' => true,

Loading…
Cancel
Save