Merge pull request #39 from docusealco/wip
This commit is contained in:
+56
-56
@@ -1,29 +1,29 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actioncable (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activejob (= 7.2.1)
|
||||
activerecord (= 7.2.1)
|
||||
activestorage (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actionmailbox (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activejob (= 7.2.1.1)
|
||||
activerecord (= 7.2.1.1)
|
||||
activestorage (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
actionview (= 7.2.1)
|
||||
activejob (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actionmailer (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
actionview (= 7.2.1.1)
|
||||
activejob (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.2.1)
|
||||
actionview (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actionpack (7.2.1.1)
|
||||
actionview (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4, < 3.2)
|
||||
@@ -32,35 +32,35 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activerecord (= 7.2.1)
|
||||
activestorage (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actiontext (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activerecord (= 7.2.1.1)
|
||||
activestorage (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
actionview (7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
activejob (7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
activerecord (7.2.1)
|
||||
activemodel (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
activemodel (7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
activerecord (7.2.1.1)
|
||||
activemodel (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activejob (= 7.2.1)
|
||||
activerecord (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
activestorage (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activejob (= 7.2.1.1)
|
||||
activerecord (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.2.1)
|
||||
activesupport (7.2.1.1)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
@@ -320,7 +320,7 @@ GEM
|
||||
mysql2 (0.5.6)
|
||||
net-http-persistent (4.0.4)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.16)
|
||||
net-imap (0.4.17)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -372,7 +372,7 @@ GEM
|
||||
puma (6.4.3)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.7)
|
||||
rack (3.1.8)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.0.0)
|
||||
@@ -382,20 +382,20 @@ GEM
|
||||
rackup (2.1.0)
|
||||
rack (>= 3)
|
||||
webrick (~> 1.8)
|
||||
rails (7.2.1)
|
||||
actioncable (= 7.2.1)
|
||||
actionmailbox (= 7.2.1)
|
||||
actionmailer (= 7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
actiontext (= 7.2.1)
|
||||
actionview (= 7.2.1)
|
||||
activejob (= 7.2.1)
|
||||
activemodel (= 7.2.1)
|
||||
activerecord (= 7.2.1)
|
||||
activestorage (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
rails (7.2.1.1)
|
||||
actioncable (= 7.2.1.1)
|
||||
actionmailbox (= 7.2.1.1)
|
||||
actionmailer (= 7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
actiontext (= 7.2.1.1)
|
||||
actionview (= 7.2.1.1)
|
||||
activejob (= 7.2.1.1)
|
||||
activemodel (= 7.2.1.1)
|
||||
activerecord (= 7.2.1.1)
|
||||
activestorage (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.2.1)
|
||||
railties (= 7.2.1.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -410,9 +410,9 @@ GEM
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (7.2.1)
|
||||
actionpack (= 7.2.1)
|
||||
activesupport (= 7.2.1)
|
||||
railties (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
@@ -559,7 +559,7 @@ GEM
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.18)
|
||||
zeitwerk (2.7.0)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# 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.**
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
@@ -405,7 +405,8 @@ export default {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
:name="`values[${field.uuid}]`"
|
||||
@keydown.enter="onEnter"
|
||||
@focus="$emit('focus')"
|
||||
@paste="onPaste"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,6 +99,25 @@ export default {
|
||||
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 () {
|
||||
const inputEl = this.$refs.input
|
||||
|
||||
|
||||
@@ -693,6 +693,7 @@ export default {
|
||||
'de-DE': 'DD.MM.YYYY',
|
||||
'fr-FR': 'DD/MM/YYYY',
|
||||
'it-IT': 'DD/MM/YYYY',
|
||||
'en-GB': 'DD/MM/YYYY',
|
||||
'es-ES': 'DD/MM/YYYY'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -55,6 +55,8 @@ class Submission < ApplicationRecord
|
||||
has_one_attached :audit_trail
|
||||
has_one_attached :combined_document
|
||||
|
||||
has_many_attached :preview_documents
|
||||
|
||||
has_many :template_schema_documents,
|
||||
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
||||
through: :template, source: :documents_attachments
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="text-center">
|
||||
<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' %>
|
||||
Global
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% 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' %>
|
||||
<div class="flex flex-col justify-between h-full overflow-hidden">
|
||||
<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 :submitters, only: %i[index show update]
|
||||
resources :submissions, only: %i[index show create destroy] do
|
||||
resources :documents, only: %i[index], controller: 'submission_documents'
|
||||
collection do
|
||||
resources :init, only: %i[create], controller: 'submissions'
|
||||
resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ module Accounts
|
||||
new_user.uuid = SecureRandom.uuid
|
||||
new_user.account = new_account
|
||||
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|
|
||||
new_template = template.dup
|
||||
|
||||
@@ -16,7 +16,15 @@ module ActionMailerConfigsInterceptor
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ module ActionMailerEventsObserver
|
||||
emailable_type:,
|
||||
event_type: :send,
|
||||
email:,
|
||||
data: { method: mail.delivery_method.class.name.underscore },
|
||||
data: { from: mail.from, method: mail.delivery_method.class.name.underscore },
|
||||
event_datetime: Time.current
|
||||
)
|
||||
end
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ module Docuseal
|
||||
TWITTER_URL = 'https://twitter.com/docusealco'
|
||||
TWITTER_HANDLE = '@docusealco'
|
||||
CHATGPT_URL = 'https://chatgpt.com/g/g-9hg8AAw0r-docuseal'
|
||||
SUPPORT_EMAIL = 'support@docuseal.co'
|
||||
SUPPORT_EMAIL = 'support@docuseal.com'
|
||||
HOST = ENV.fetch('HOST', 'localhost')
|
||||
AATL_CERT_NAME = 'docuseal_aatl'
|
||||
CONSOLE_URL = if Rails.env.development?
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
module Params
|
||||
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)
|
||||
|
||||
def self.call(...)
|
||||
@@ -65,6 +67,27 @@ module Params
|
||||
raise_error(message || "#{key} must follow the #{regexp.source} format")
|
||||
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 = [])
|
||||
old_path = @current_path
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ module Params
|
||||
required(params, %i[emails email])
|
||||
|
||||
type(params, :emails, String)
|
||||
email_format(params, :emails, message: 'emails are invalid')
|
||||
boolean(params, :send_email)
|
||||
type(params, :message, Hash)
|
||||
|
||||
@@ -43,8 +44,8 @@ module Params
|
||||
type(params, :completed_redirect_url, String)
|
||||
type(params, :bcc_completed, String)
|
||||
type(params, :reply_to, String)
|
||||
format(params, :bcc_completed, /@/, message: 'bcc_completed email is invalid')
|
||||
format(params, :reply_to, /@/, message: 'reply_to email is invalid')
|
||||
email_format(params, :bcc_completed, message: 'bcc_completed email is invalid')
|
||||
email_format(params, :reply_to, message: 'reply_to email is invalid')
|
||||
type(params, :message, Hash)
|
||||
type(params, :submitters, Array)
|
||||
|
||||
@@ -60,6 +61,7 @@ module Params
|
||||
if params[:submitters].present?
|
||||
in_path(params, :submitters) do |submitters_params|
|
||||
type(submitters_params, 0, Hash)
|
||||
unique_value(submitters_params, :role)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -74,8 +76,8 @@ module Params
|
||||
type(submitter_params, :name, String)
|
||||
type(submitter_params, :reply_to, String)
|
||||
type(submitter_params, :email, String)
|
||||
format(submitter_params, :email, /@/, message: 'email is invalid')
|
||||
format(submitter_params, :reply_to, /@/, message: 'reply_to email is invalid')
|
||||
email_format(submitter_params, :email, message: 'email is invalid')
|
||||
email_format(submitter_params, :reply_to, message: 'reply_to email is invalid')
|
||||
type(submitter_params, :phone, String)
|
||||
format(submitter_params, :phone, /\A\+\d+\z/,
|
||||
message: 'phone should start with +<country code> and contain only digits')
|
||||
@@ -105,7 +107,7 @@ module Params
|
||||
type(params, :order, String)
|
||||
type(params, :completed_redirect_url, 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)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
||||
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
|
||||
|
||||
is_flatten =
|
||||
submitter.account.account_configs
|
||||
.find_or_initialize_by(key: AccountConfig::FLATTEN_RESULT_PDF_KEY).value != false
|
||||
|
||||
account = submitter.account
|
||||
attachments_data_cache = {}
|
||||
|
||||
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?
|
||||
pdfs_index.each_value do |pdf|
|
||||
@@ -145,6 +139,16 @@ module Submissions
|
||||
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|
|
||||
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(':'))
|
||||
end
|
||||
|
||||
def build_pdfs_index(submitter, flatten: true)
|
||||
latest_submitter = find_last_submitter(submitter)
|
||||
def build_pdfs_index(submission, submitter: nil, flatten: true)
|
||||
latest_submitter = find_last_submitter(submission, submitter:)
|
||||
|
||||
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
||||
|
||||
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|
|
||||
pdf =
|
||||
@@ -582,11 +586,11 @@ module Submissions
|
||||
font_wrapper.custom_glyph(replace_with, character)
|
||||
end
|
||||
|
||||
def find_last_submitter(submitter)
|
||||
submitter.submission.submitters
|
||||
.select(&:completed_at?)
|
||||
.select { |e| e.id != submitter.id && e.completed_at <= submitter.completed_at }
|
||||
.max_by(&:completed_at)
|
||||
def find_last_submitter(submission, submitter: nil)
|
||||
submission.submitters
|
||||
.select(&:completed_at?)
|
||||
.select { |e| submitter.nil? ? true : e.id != submitter.id && e.completed_at <= submitter.completed_at }
|
||||
.max_by(&:completed_at)
|
||||
end
|
||||
|
||||
def build_pdf_from_image(attachment)
|
||||
|
||||
+43
-32
@@ -7,7 +7,11 @@ FactoryBot.define do
|
||||
author factory: %i[user]
|
||||
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!(
|
||||
io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
|
||||
filename: 'sample-document.pdf',
|
||||
@@ -22,39 +26,46 @@ FactoryBot.define do
|
||||
Templates::ProcessDocument.call(attachment, attachment.download)
|
||||
|
||||
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
|
||||
template.submitters = [
|
||||
number_words = %w[first second third fourth fifth sixth seventh eighth ninth tenth]
|
||||
|
||||
template.submitters = Array.new(evaluator.submitter_count) do |i|
|
||||
{
|
||||
'name' => 'First Party',
|
||||
'uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c'
|
||||
'name' => "#{number_words[i]&.capitalize} Party",
|
||||
'uuid' => SecureRandom.uuid
|
||||
}
|
||||
]
|
||||
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',
|
||||
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
|
||||
'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!
|
||||
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!(:folder) { create(:template_folder, account:) }
|
||||
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
|
||||
it 'returns a list of submissions' do
|
||||
@@ -56,13 +57,16 @@ describe 'Submission API', type: :request do
|
||||
|
||||
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/submissions/emails' do
|
||||
it 'creates a submission using email' do
|
||||
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: templates[0].id,
|
||||
emails: 'john.doe@example.com'
|
||||
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)
|
||||
@@ -70,6 +74,81 @@ describe 'Submission API', type: :request do
|
||||
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
|
||||
|
||||
describe 'POST /api/submissions/emails' do
|
||||
it 'creates a submission using email' do
|
||||
post '/api/submissions/emails', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||
template_id: templates[0].id,
|
||||
emails: 'john.doe@example.com,jane.doe@example.com'
|
||||
}.to_json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
submissions = Submission.last(2)
|
||||
submissions_body = submissions.reduce([]) { |acc, submission| acc + create_submission_body(submission) }
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -144,8 +144,8 @@ describe 'Templates API', type: :request do
|
||||
name: template.name,
|
||||
fields: [
|
||||
{
|
||||
'uuid' => '21637fc9-0655-45df-8952-04ec64949e85',
|
||||
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
|
||||
'uuid' => template.fields[0]['uuid'],
|
||||
'submitter_uuid' => template.submitters[0]['uuid'],
|
||||
'name' => 'First Name',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
@@ -161,8 +161,8 @@ describe 'Templates API', type: :request do
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => '1f97f8e3-dc82-4586-aeea-6ebed6204e46',
|
||||
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
|
||||
'uuid' => template.fields[1]['uuid'],
|
||||
'submitter_uuid' => template.submitters[0]['uuid'],
|
||||
'name' => '',
|
||||
'type' => 'signature',
|
||||
'required' => true,
|
||||
|
||||
Reference in New Issue
Block a user