Merge from docusealco/wip

pull/381/merge 2.1.3
Alex Turchyn 2 months ago committed by GitHub
commit 9b935a8180
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,

@ -13,7 +13,7 @@ module Api
def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
if blob_uuid.blank? || purp != 'blob'
Rollbar.error('Blob not found') if defined?(Rollbar)
return head :not_found
@ -24,8 +24,9 @@ module Api
attachment = blob.attachments.take
@record = attachment.record
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
authorization_check!(attachment) if exp.blank?
authorization_check!(attachment, @record, exp)
if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range']
@ -41,14 +42,19 @@ module Api
private
def authorization_check!(attachment)
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) ||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
!attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
def authorization_check!(attachment, record, exp)
return if attachment.name == 'logo'
return if exp.to_i >= Time.current.to_i
return if is_authorized
return if current_user && current_ability.can?(:read, record)
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
return if !require_ttl && !require_auth
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)

@ -18,12 +18,14 @@ module Api
field: :completed_at
)
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: submitters.map do |s|
{
event_type: 'form.completed',
timestamp: s.completed_at,
data: Submitters::SerializeForWebhook.call(s)
data: Submitters::SerializeForWebhook.call(s, expires_at:)
}
end,
pagination: {

@ -34,10 +34,12 @@ module Api
associations: [:blob]
).call
expires_at = Accounts.link_expires_at(current_account)
render json: {
id: @submission.id,
documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
end
}
end

@ -14,16 +14,19 @@ module Api
:created_by_user, :submission_events,
template: :folder,
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
audit_trail_attachment: :blob
audit_trail_attachment: :blob,
combined_document_attachment: :blob
),
field: :completed_at)
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: submissions.map do |s|
{
event_type: 'submission.completed',
timestamp: s.completed_at,
data: Submissions::SerializeForApi.call(s, s.submitters)
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
}
end,
pagination: {

@ -18,10 +18,12 @@ module Api
combined_document_attachment: :blob,
audit_trail_attachment: :blob))
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: submissions.map do |s|
Submissions::SerializeForApi.call(s, s.submitters, params,
with_events: false, with_documents: false, with_values: false)
with_events: false, with_documents: false, with_values: false, expires_at:)
end,
pagination: {
count: submissions.size,

@ -14,9 +14,11 @@ module Api
documents_attachments: :blob, attachments_attachments: :blob)
)
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: submitters.map do |s|
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
end,
pagination: {
count: submitters.size,

@ -26,13 +26,15 @@ module Api
original_template: @template,
documents: params[:documents])
Templates.maybe_assign_access(cloned_template)
cloned_template.save!
WebhookUrls.enqueue_events(cloned_template, 'template.created')
SearchEntries.enqueue_reindex(cloned_template)
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
end
end
end

@ -24,13 +24,14 @@ module Api
name: :preview_images)
.preload(:blob)
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: templates.map do |t|
Templates::SerializeForApi.call(
t,
schema_documents.select { |e| e.record_id == t.id },
preview_image_attachments
)
Templates::SerializeForApi.call(t,
schema_documents: schema_documents.select { |e| e.record_id == t.id },
preview_image_attachments:,
expires_at:)
end,
pagination: {
count: templates.size,

@ -10,11 +10,13 @@ class SubmissionsExportController < ApplicationController
attachments_attachments: :blob })
.order(id: :asc)
expires_at = Accounts.link_expires_at(current_account)
if params[:format] == 'csv'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
filename: "#{@template.name}.csv"
elsif params[:format] == 'xlsx'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
filename: "#{@template.name}.xlsx"
end
end

@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
Templates.maybe_assign_access(cloned_template)
cloned_template.save!
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,

@ -69,6 +69,8 @@ class TemplatesController < ApplicationController
@template.account = current_account
end
Templates.maybe_assign_access(@template)
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template

@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
Templates.maybe_assign_access(template)
template.save!
template

@ -60,7 +60,15 @@ export default {
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
},
dom () {
const text = this.string.replace(/(?<!\(\s*)(https?:\/\/[^\s)]+)(?!\s*\))/g, (url) => `[${url}](${url})`)
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
const text = linkParts.map((part, index) => {
if (part.match(/^https?:\/\//) && !linkParts[index - 1]?.match(/\(\s*$/) && !linkParts[index + 1]?.match(/^\s*\)/)) {
return `[${part}](${part})`
} else {
return part
}
}).join('')
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
}

@ -1265,24 +1265,31 @@ export default {
if (!field.areas.length) {
this.template.fields.splice(this.template.fields.indexOf(field), 1)
this.template.fields.forEach((f) => {
(f.conditions || []).forEach((c) => {
this.removeFieldConditions(field)
}
this.save()
},
removeFieldConditions (field) {
this.template.fields.forEach((f) => {
if (f.conditions) {
f.conditions.forEach((c) => {
if (c.field_uuid === field.uuid) {
f.conditions.splice(f.conditions.indexOf(c), 1)
}
})
})
}
})
this.template.schema.forEach((item) => {
(item.conditions || []).forEach((c) => {
this.template.schema.forEach((item) => {
if (item.conditions) {
item.conditions.forEach((c) => {
if (c.field_uuid === field.uuid) {
item.conditions.splice(item.conditions.indexOf(c), 1)
}
})
})
}
this.save()
}
})
},
pasteField () {
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
@ -1715,8 +1722,15 @@ export default {
})
})
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.template.fields = this.template.fields.reduce((acc, f) => {
if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
this.removeFieldConditions(f)
} else {
acc.push(f)
}
return acc
}, [])
this.save()
}

@ -38,6 +38,7 @@ class AccountConfig < ApplicationRecord
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire'
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
WITH_SIGNATURE_ID = 'with_signature_id'

@ -114,16 +114,16 @@ class Submission < ApplicationRecord
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
end
def audit_trail_url
def audit_trail_url(expires_at: nil)
return if audit_trail.blank?
ActiveStorage::Blob.proxy_url(audit_trail.blob)
ActiveStorage::Blob.proxy_url(audit_trail.blob, expires_at:)
end
alias audit_log_url audit_trail_url
def combined_document_url
def combined_document_url(expires_at: nil)
return if combined_document.blank?
ActiveStorage::Blob.proxy_url(combined_document.blob)
ActiveStorage::Blob.proxy_url(combined_document.blob, expires_at:)
end
end

@ -130,6 +130,18 @@
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('expirable_file_download_links') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>

@ -26,6 +26,7 @@ en: &en
select: Select
party: Party
edit_order: Edit Order
expirable_file_download_links: Expirable file download links
invite_form_fields: Invite form fields
default_parties: Default parties
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
@ -36,6 +37,7 @@ en: &en
bcc_recipients: BCC recipients
resend_pending: Re-send pending
always_enforce_signing_order: Always enforce the signing order
create_templates_with_private_access_by_default: Create templates with private access by default
ensure_unique_recipients: Ensure unique recipients
edit_per_party: Edit per party
reply_to: Reply to
@ -900,6 +902,8 @@ en: &en
range_without_total: "%{from}-%{to} events"
es: &es
expirable_file_download_links: Enlaces de descarga de archivos con vencimiento
create_templates_with_private_access_by_default: Crear plantillas con acceso privado por defecto
party: Parte
edit_order: Edita Pedido
select: Seleccionar
@ -1780,6 +1784,8 @@ es: &es
range_without_total: "%{from}-%{to} eventos"
it: &it
expirable_file_download_links: Link di download di file con scadenza
create_templates_with_private_access_by_default: Crea modelli con accesso privato per impostazione predefinita
party: Parte
edit_order: Modifica Ordine
select: Seleziona
@ -2660,6 +2666,8 @@ it: &it
range_without_total: "%{from}-%{to} eventi"
fr: &fr
expirable_file_download_links: Liens de téléchargement de fichiers expirables
create_templates_with_private_access_by_default: Créer des modèles avec un accès privé par défaut
party: Partie
edit_order: Modifier la commande
select: Sélectionner
@ -3543,6 +3551,8 @@ fr: &fr
range_without_total: "%{from} à %{to} événements"
pt: &pt
expirable_file_download_links: Links de download de arquivos com expiração
create_templates_with_private_access_by_default: Criar modelos com acesso privado por padrão
party: Parte
edit_order: Edita Pedido
select: Selecionar
@ -4424,6 +4434,8 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos"
de: &de
expirable_file_download_links: Ablaufbare Datei-Download-Links
create_templates_with_private_access_by_default: Vorlagen standardmäßig mit privatem Zugriff erstellen
party: Partei
edit_order: Bestellung bearbeiten
select: Auswählen

@ -0,0 +1,30 @@
# frozen_string_literal: true
class PopulateExpireLinkConfigs < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
class MigrationAccount < ActiveRecord::Base
self.table_name = 'accounts'
end
class MigrationAccountConfig < ActiveRecord::Base
self.table_name = 'account_configs'
serialize :value, coder: JSON
end
def up
MigrationAccount.find_each do |account|
config = MigrationAccountConfig.find_or_initialize_by(key: 'download_links_expire', account_id: account.id)
next if config.persisted?
config.value = false
config.save!
end
end
def down
nil
end
end

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_07_18_121133) do
ActiveRecord::Schema[8.0].define(version: 2025_08_31_125322) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "plpgsql"

@ -1,6 +1,8 @@
# frozen_string_literal: true
module Accounts
LINK_EXPIRES_AT = 40.minutes
module_function
def create_duplicate(account)
@ -185,4 +187,11 @@ module Accounts
rescue TZInfo::InvalidTimezoneIdentifier
'UTC'
end
def link_expires_at(account)
return if AccountConfig.find_or_initialize_by(account: account,
key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY).value == false
LINK_EXPIRES_AT.from_now
end
end

@ -102,6 +102,7 @@ module ReplaceEmailVariables
return unless submitter
value = submitter.try(field_name)
expires_at = nil
if value_name
field = (submission.template_fields || submission.template.fields).find { |e| e['name'] == value_name }
@ -114,7 +115,11 @@ module ReplaceEmailVariables
attachment = submitter.attachments.find { |e| e.uuid == attachment_uuid }
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
if attachment
expires_at ||= Accounts.link_expires_at(Account.new(id: submission.account_id))
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
end
else
value[field&.dig('uuid')]
end

@ -239,7 +239,7 @@ module Submissions
item['conditions'].each_with_object([]) do |condition, acc|
result =
if fields_index[condition['field_uuid']]['submitter_uuid'] == include_submitter_uuid
if fields_index.dig(condition['field_uuid'], 'submitter_uuid') == include_submitter_uuid
submitter_conditions_acc << condition if submitter_conditions_acc
true

@ -391,8 +391,8 @@ module Submissions
composer.formatted_text_box(
Array.wrap(value).map do |uuid|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
link =
ActiveStorage::Blob.proxy_url(attachment.blob)
link = r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
{ link:, text: "#{attachment.filename}\n", style: :link }
end,
@ -489,6 +489,10 @@ module Submissions
padding: [5, 0, 0, 8],
position: :float, text_align: :left)
end
def r
Rails.application.routes.url_helpers
end
# rubocop:enable Metrics
end
end

@ -6,8 +6,8 @@ module Submissions
module_function
def call(submissions, format: :csv)
rows = build_table_rows(submissions)
def call(submissions, format: :csv, expires_at: nil)
rows = build_table_rows(submissions, expires_at:)
if format.to_sym == :csv
rows_to_csv(rows)
@ -57,7 +57,7 @@ module Submissions
headers.map { |key| row.find { |e| e[:name] == key }&.dig(:value) }
end
def build_table_rows(submissions)
def build_table_rows(submissions, expires_at: nil)
submissions.preload(submitters: [attachments_attachments: :blob, documents_attachments: :blob])
.find_each.map do |submission|
submission_data = []
@ -69,7 +69,7 @@ module Submissions
submission_data += build_submission_data(submitter, submitter_name, submitters_count)
submission_data += submitter_formatted_fields(submitter).map do |field|
submission_data += submitter_formatted_fields(submitter, expires_at:).map do |field|
{
name: column_name(field[:name], submitter_name, submitters_count),
value: field[:value]
@ -81,7 +81,7 @@ module Submissions
submission_data += submitter.documents.map.with_index(1) do |attachment, index|
{
name: "#{I18n.t('document')} #{index}",
value: ActiveStorage::Blob.proxy_url(attachment.blob)
value: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
}
end
end
@ -123,7 +123,7 @@ module Submissions
submitters_count > 1 ? "#{submitter_name} - #{name}" : name
end
def submitter_formatted_fields(submitter)
def submitter_formatted_fields(submitter, expires_at: nil)
fields = submitter.submission.template_fields || submitter.submission.template.fields
template_fields = fields.select { |f| f['submitter_uuid'] == submitter.uuid }
@ -142,11 +142,13 @@ module Submissions
value =
if template_field_type.in?(%w[image signature])
attachment = attachments_index[submitter_value]
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
elsif template_field_type == 'file'
Array.wrap(submitter_value).compact_blank.filter_map do |e|
attachment = attachments_index[e]
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
end
else
submitter_value

@ -201,13 +201,15 @@ module Submissions
attachments_data_cache = {}
return pdfs_index if submitter.submission.template_fields.blank?
submission = submitter.submission
return pdfs_index if submission.template_fields.blank?
with_headings = find_last_submitter(submitter.submission, submitter:).blank? if with_headings.nil?
with_headings = find_last_submitter(submission, submitter:).blank? if with_headings.nil?
locale = submitter.metadata.fetch('lang', account.locale)
submitter.submission.template_fields.each do |field|
submission.template_fields.each do |field|
next if field['type'] == 'heading' && !with_headings
next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading'
@ -476,7 +478,7 @@ module Submissions
height_diff - (height_diff.zero? ? diff : 0)
],
A: { Type: :Action, S: :URI,
URI: ActiveStorage::Blob.proxy_url(attachment.blob) }
URI: r.submissions_preview_url(submission.slug, **Docuseal.default_url_options) }
}
)
@ -747,11 +749,15 @@ module Submissions
def maybe_rotate_pdf(pdf)
return pdf if pdf.pages.size > MAX_PAGE_ROTATE
is_pages_rotated = pdf.pages.root[:Rotate].present? && pdf.pages.root[:Rotate] != 0
pdf.pages.root[:Rotate] = 0 if is_pages_rotated
is_rotated = pdf.pages.filter_map do |page|
page.rotate(0, flatten: true) if page[:Rotate] != 0
end.present?
return pdf unless is_rotated
return pdf if !is_rotated && !is_pages_rotated
io = StringIO.new
@ -873,7 +879,7 @@ module Submissions
end
end
def h
def r
Rails.application.routes.url_helpers
end
end

@ -4,7 +4,6 @@ module Submissions
module SerializeForApi
SERIALIZE_PARAMS = {
only: %i[id name slug source submitters_order expire_at created_at updated_at archived_at],
methods: %i[audit_log_url combined_document_url],
include: {
submitters: { only: %i[id] },
template: { only: %i[id name external_id created_at updated_at],
@ -15,11 +14,13 @@ module Submissions
module_function
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true)
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true,
expires_at: Accounts.link_expires_at(Account.new(id: submission.account_id)))
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
serialized_submitters = submitters.map do |submitter|
Submitters::SerializeForApi.call(submitter, with_documents:, with_events: false, with_values:, params:)
Submitters::SerializeForApi.call(submitter, with_documents:, with_events: false, with_values:, params:,
expires_at:)
end
json = submission.as_json(SERIALIZE_PARAMS)
@ -30,8 +31,6 @@ module Submissions
json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events)
end
json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params)
if submitters.all?(&:completed_at?)
last_submitter = submitters.max_by(&:completed_at)
@ -39,10 +38,16 @@ module Submissions
json['documents'] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
end
json['audit_log_url'] = submission.audit_log_url(expires_at:)
json['combined_document_url'] = submission.combined_document_url(expires_at:)
json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params, expires_at:)
json['status'] = 'completed'
json['completed_at'] = last_submitter.completed_at.as_json
else
json['documents'] = [] if with_documents
json['audit_log_url'] = nil
json['combined_document_url'] = nil
json['status'] = build_status(submission, submitters)
json['completed_at'] = nil
end
@ -60,7 +65,7 @@ module Submissions
end
end
def maybe_build_combined_url(submitters, submission, params)
def maybe_build_combined_url(submitters, submission, params, expires_at: nil)
return unless submitters.all?(&:completed_at?)
attachment = submission.combined_document_attachment
@ -71,7 +76,7 @@ module Submissions
attachment = Submissions::GenerateCombinedAttachment.call(submitter)
end
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
end
end
end

@ -11,7 +11,7 @@ module Submitters
module_function
def call(submitter, with_template: false, with_events: false, with_documents: true, with_urls: false,
with_values: true, params: {})
with_values: true, params: {}, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id)))
ActiveRecord::Associations::Preloader.new(
records: [submitter],
associations: if with_documents
@ -24,15 +24,19 @@ module Submitters
additional_attrs = {}
if params[:include].to_s.include?('fields')
additional_attrs['fields'] = SerializeForWebhook.build_fields_array(submitter)
additional_attrs['fields'] = SerializeForWebhook.build_fields_array(submitter, expires_at:)
end
if with_template
additional_attrs['template'] = submitter.submission.template.as_json(only: %i[id name created_at updated_at])
end
additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter) if with_values
additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter) if with_documents
additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter, expires_at:) if with_values
if with_documents
additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter, expires_at:)
end
additional_attrs['preferences'] = submitter.preferences.except('default_values')
additional_attrs['submission_events'] = serialize_events(submitter.submission_events) if with_events

@ -10,13 +10,13 @@ module Submitters
module_function
def call(submitter)
def call(submitter, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id)))
ActiveRecord::Associations::Preloader.new(
records: [submitter], associations: [documents_attachments: :blob, attachments_attachments: :blob]
).call
values = build_values_array(submitter)
documents = build_documents_array(submitter)
values = build_values_array(submitter, expires_at:)
documents = build_documents_array(submitter, expires_at:)
submission = submitter.submission
@ -32,7 +32,7 @@ module Submitters
'preferences' => submitter.preferences.except('default_values'),
'values' => values,
'documents' => documents,
'audit_log_url' => submitter.submission.audit_log_url,
'audit_log_url' => submitter.submission.audit_log_url(expires_at:),
'submission_url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
'template' => submission.template.as_json(
only: %i[id name external_id created_at updated_at],
@ -40,15 +40,15 @@ module Submitters
),
'submission' => {
'id' => submission.id,
'audit_log_url' => submission.audit_log_url,
'combined_document_url' => submission.combined_document_url,
'audit_log_url' => submission.audit_log_url(expires_at:),
'combined_document_url' => submission.combined_document_url(expires_at:),
'status' => build_submission_status(submission),
'url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
'created_at' => submission.created_at.as_json
})
end
def build_values_array(submitter)
def build_values_array(submitter, expires_at: nil)
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
attachments_index = submitter.attachments.index_by(&:uuid)
submitter_field_counters = Hash.new { 0 }
@ -64,13 +64,13 @@ module Submitters
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index, expires_at:)
{ 'field' => field_name, 'value' => value }
end
end
def build_fields_array(submitter)
def build_fields_array(submitter, expires_at: nil)
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
attachments_index = submitter.attachments.index_by(&:uuid)
submitter_field_counters = Hash.new { 0 }
@ -86,7 +86,7 @@ module Submitters
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index, expires_at:)
{ 'name' => field_name, 'uuid' => field['uuid'], 'value' => value, 'readonly' => field['readonly'] == true }
end
@ -104,26 +104,26 @@ module Submitters
end
end
def build_documents_array(submitter)
def build_documents_array(submitter, expires_at: nil)
submitter.documents.map do |attachment|
{ 'name' => attachment.filename.base, 'url' => rails_storage_proxy_url(attachment) }
{ 'name' => attachment.filename.base, 'url' => rails_storage_proxy_url(attachment, expires_at:) }
end
end
def fetch_field_value(field, value, attachments_index)
def fetch_field_value(field, value, attachments_index, expires_at: nil)
if field['type'].in?(%w[image signature initials stamp payment])
rails_storage_proxy_url(attachments_index[value])
rails_storage_proxy_url(attachments_index[value], expires_at:)
elsif field['type'] == 'file'
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e]) }
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e], expires_at:) }
else
value
end
end
def rails_storage_proxy_url(attachment)
def rails_storage_proxy_url(attachment, expires_at: nil)
return if attachment.blank?
ActiveStorage::Blob.proxy_url(attachment.blob)
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
end
def r

@ -37,6 +37,10 @@ module Templates
hash
end
def maybe_assign_access(_template)
nil
end
def search(current_user, templates, keyword)
if Docuseal.fulltext_search?
fulltext_search(current_user, templates, keyword)

@ -14,7 +14,8 @@ module Templates
module_function
def call(template, schema_documents = template.schema_documents.preload(:blob), preview_image_attachments = nil)
def call(template, schema_documents: template.schema_documents.preload(:blob), preview_image_attachments: nil,
expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
json = template.as_json(SERIALIZE_PARAMS)
preview_image_attachments ||=
@ -40,8 +41,8 @@ module Templates
{
'id' => attachment.id,
'uuid' => attachment.uuid,
'url' => ActiveStorage::Blob.proxy_url(attachment.blob),
'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob),
'url' => ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:),
'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob, expires_at:),
'filename' => attachment.filename
}
end

@ -8,6 +8,10 @@ describe 'Templates API' do
let(:folder) { create(:template_folder, account:) }
let(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } }
before do
allow(Accounts).to receive(:link_expires_at).and_return(Accounts::LINK_EXPIRES_AT)
end
describe 'GET /api/templates' do
it 'returns a list of templates' do
templates = [
@ -211,8 +215,8 @@ describe 'Templates API' do
{
id: template.documents.first.id,
uuid: template.documents.first.uuid,
url: ActiveStorage::Blob.proxy_url(attachment.blob),
preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob),
url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: Accounts::LINK_EXPIRES_AT),
preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob, expires_at: Accounts::LINK_EXPIRES_AT),
filename: 'sample-document.pdf'
}
],
@ -235,7 +239,7 @@ describe 'Templates API' do
folder_name: folder.name,
source: 'native',
external_id: template.external_id,
application_key: template.external_id # Backward compatibility
application_key: template.external_id
}
end

Loading…
Cancel
Save