Compare commits

...
19 Commits
Author SHA1 Message Date
Pete Matsyburka 95f734e4b8 adjust cloud logs 2024-02-11 15:25:10 +02:00
Pete Matsyburka ff74574e84 adjust api completion 2024-02-10 21:03:12 +02:00
Pete Matsyburka 843f6c405f max 20 signer parties 2024-02-10 11:31:20 +02:00
Pete Matsyburka f8b24ebfce retry webhooks on non 200 status 2024-02-10 11:31:19 +02:00
Pete Matsyburka d1d26b5424 increase canvas resolution 2024-02-09 22:33:18 +02:00
Pete Matsyburka 8aa6f45da8 fix signature image reupload 2024-02-09 21:24:30 +02:00
Pete Matsyburka 48d5bee95b parse unix date 2024-02-09 15:03:20 +02:00
Pete Matsyburka 98c79d2dec skip anotation if nil 2024-02-09 00:43:13 +02:00
Pete Matsyburka 0f51238f62 fix default date 2024-02-08 23:29:18 +02:00
Pete Matsyburka b7323b929f set signing date option 2024-02-08 22:55:13 +02:00
Pete Matsyburka 05eb1537f7 replace checkmark emoji 2024-02-08 21:44:27 +02:00
Pete Matsyburka bc9f27cfed allow to aunarchive template 2024-02-08 20:49:10 +02:00
Pete Matsyburka 8f457be719 fix default values 2024-02-08 20:12:08 +02:00
Pete Matsyburka 6d590e13c9 flatten pdf form when signing 2024-02-08 18:06:55 +02:00
Pete Matsyburka c987ec172a fix canvas intersection observer 2024-02-08 12:11:10 +02:00
Pete Matsyburka 49f1637c47 add more submitter parties 2024-02-08 11:54:54 +02:00
Pete Matsyburka 13379bd699 resize canvas on visibility change 2024-02-07 01:47:57 +02:00
Pete Matsyburka 5ee762c08c adjust update tempalte api 2024-02-06 20:07:15 +02:00
Pete Matsyburka edb6d561f1 allow to add checked checkboxes in builder 2024-02-05 23:03:45 +02:00
27 changed files with 332 additions and 86 deletions

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ Metrics/ParameterLists:
Max: 10
Metrics/MethodLength:
Max: 25
Max: 30
Exclude:
- 'db/migrate/**'
+23 -6
View File
@@ -33,12 +33,18 @@ module Api
def show
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
serialized_submitters = submitters.map do |submitter|
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
Submitters::SerializeForApi.call(submitter)
submitters.each do |submitter|
if submitter.completed_at? && submitter.documents_attachments.blank?
submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
end
end
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
end
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
json = @submission.as_json(
serialize_params.deep_merge(
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
@@ -80,6 +86,12 @@ module Api
Submissions.send_signature_requests(submissions)
submissions.each do |submission|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
end
end
render json: submissions.flat_map(&:submitters)
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
render json: { error: e.message }, status: :unprocessable_entity
@@ -118,8 +130,13 @@ module Api
params:
)
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments,
submissions.flat_map(&:submitters))
submitters = submissions.flat_map(&:submitters)
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
submitters.each do |submitter|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
end
submissions
end
@@ -55,6 +55,8 @@ module Api
@submitter.save!
@submitter.submission.save!
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
end
if @submitter.completed_at?
+21 -9
View File
@@ -24,10 +24,16 @@ module Api
end
def update
if (folder_name = params.dig(:template, :folder_name))
if (folder_name = params[:folder_name] || params.dig(:template, :folder_name))
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
end
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
if archived.in?([true, false])
@template.archived_at = archived == true ? Time.current : nil
end
@template.update!(template_params)
render json: @template.as_json(only: %i[id updated_at])
@@ -64,15 +70,21 @@ module Api
end
def template_params
params.require(:template).permit(
permit_params = [
:name,
schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
{ preferences: {},
options: [%i[value uuid]],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
)
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
{ preferences: {},
options: [%i[value uuid]],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
]
if params.key?(:template)
params.require(:template).permit(*permit_params)
else
params.permit(*permit_params)
end
end
end
end
+1 -1
View File
@@ -307,7 +307,7 @@ export default {
formattedDate () {
if (this.field.type === 'date' && this.modelValue) {
return this.formatDate(
new Date(this.modelValue),
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
)
} else {
+1 -1
View File
@@ -621,7 +621,7 @@ export default {
this.fields.forEach((field) => {
if (field.default_value && !field.readonly) {
this.values[field.uuid] = field.default_value
this.values[field.uuid] ||= field.default_value
}
})
@@ -84,7 +84,7 @@
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl"
class="bg-white border border-base-300 rounded-2xl w-full"
/>
<input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
@@ -106,6 +106,8 @@ import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 }
import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
const scale = 3
export default {
name: 'InitialsStep',
components: {
@@ -171,8 +173,10 @@ export default {
async mounted () {
this.$nextTick(() => {
if (this.$refs.canvas) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 5
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
this.$refs.canvas.getContext('2d').scale(scale, scale)
}
this.$refs.textInput?.focus()
@@ -190,8 +194,26 @@ export default {
this.$emit('start')
})
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
this.$refs.canvas.getContext('2d').scale(scale, scale)
this.intersectionObserver?.disconnect()
}
})
})
this.intersectionObserver.observe(this.$refs.canvas)
}
},
beforeUnmount () {
this.intersectionObserver?.disconnect()
},
methods: {
remove () {
this.$emit('update:model-value', '')
@@ -222,8 +244,8 @@ export default {
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
context.textAlign = 'center'
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
},
toggleTextInput () {
this.remove()
@@ -104,7 +104,7 @@
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
style="padding: 1px; 0"
class="bg-white border border-base-300 rounded-2xl"
class="bg-white border border-base-300 rounded-2xl w-full"
/>
<input
v-if="isTextSignature"
@@ -127,6 +127,8 @@ import AppearsOn from './appears_on'
let isFontLoaded = false
const scale = 3
export default {
name: 'SignatureStep',
components: {
@@ -198,8 +200,10 @@ export default {
async mounted () {
this.$nextTick(() => {
if (this.$refs.canvas) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 3
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
this.$refs.canvas.getContext('2d').scale(scale, scale)
}
})
@@ -215,8 +219,26 @@ export default {
this.$emit('start')
})
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
this.$refs.canvas.getContext('2d').scale(scale, scale)
this.intersectionObserver?.disconnect()
}
})
})
this.intersectionObserver.observe(this.$refs.canvas)
}
},
beforeUnmount () {
this.intersectionObserver?.disconnect()
},
methods: {
remove () {
this.$emit('update:model-value', '')
@@ -260,8 +282,8 @@ export default {
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
context.textAlign = 'center'
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
},
toggleTextInput () {
this.remove()
@@ -297,14 +319,16 @@ export default {
const context = canvas.getContext('2d')
const aspectRatio = img.width / img.height
const canvasWidth = canvas.width / scale
const canvasHeight = canvas.height / scale
let targetWidth = canvas.width
let targetHeight = canvas.height
let targetWidth = canvasWidth
let targetHeight = canvasHeight
if (canvas.width / canvas.height > aspectRatio) {
targetWidth = canvas.height * aspectRatio
if (canvasWidth / canvasHeight > aspectRatio) {
targetWidth = canvasHeight * aspectRatio
} else {
targetHeight = canvas.width / aspectRatio
targetHeight = canvasWidth / aspectRatio
}
if (targetHeight > targetWidth) {
@@ -313,10 +337,10 @@ export default {
targetHeight = targetHeight * scale
}
const x = (canvas.width - targetWidth) / 2
const y = (canvas.height - targetHeight) / 2
const x = (canvasWidth - targetWidth) / 2
const y = (canvasHeight - targetHeight) / 2
context.clearRect(0, 0, canvas.width, canvas.height)
context.clearRect(0, 0, canvasWidth, canvasHeight)
context.drawImage(img, x, y, targetWidth, targetHeight)
this.$emit('start')
@@ -324,6 +348,8 @@ export default {
}
reader.readAsDataURL(file)
event.target.value = null
}
},
async submit () {
+7 -5
View File
@@ -123,12 +123,14 @@ export default {
}
},
mounted () {
this.isTextArea = this.modelValue?.includes('\n')
if (this.modelValue) {
this.isTextArea = this.modelValue.toString().includes('\n')
if (this.isTextArea) {
this.$nextTick(() => {
this.resizeTextarea()
})
if (this.isTextArea) {
this.$nextTick(() => {
this.resizeTextarea()
})
}
}
},
methods: {
+35 -3
View File
@@ -98,21 +98,32 @@
</div>
<div
class="flex items-center h-full w-full"
dir="auto"
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
>
<span
v-if="field"
class="flex justify-center items-center space-x-1 h-full"
:class="{'w-full h-full': field.type == 'checkbox'}"
>
<div
v-if="field?.default_value"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
:class="{ 'w-full h-full': field.type == 'checkbox', 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
>
<div
ref="textContainer"
class="flex items-center px-0.5"
:class="{'w-full h-full': field.type == 'checkbox'}"
>
<span class="whitespace-pre-wrap">{{ field.default_value }}</span>
<IconCheck
v-if="field.type == 'checkbox'"
class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
<span
v-else
class="whitespace-pre-wrap"
>{{ field.default_value }}</span>
</div>
</div>
<component
@@ -141,13 +152,14 @@
import FieldSubmitter from './field_submitter'
import FieldType from './field_type'
import Field from './field'
import { IconX } from '@tabler/icons-vue'
import { IconX, IconCheck } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
name: 'FieldArea',
components: {
FieldType,
IconCheck,
FieldSubmitter,
IconX
},
@@ -225,6 +237,16 @@ export default {
},
borderColors () {
return [
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
'border-yellow-300/80',
'border-purple-600/80',
'border-pink-500/80',
'border-cyan-500/80',
'border-orange-500/80',
'border-lime-500/80',
'border-indigo-500/80',
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
@@ -239,6 +261,16 @@ export default {
},
bgColors () {
return [
'bg-red-100/80',
'bg-sky-100/80',
'bg-emerald-100/80',
'bg-yellow-100/80',
'bg-purple-100/80',
'bg-pink-100/80',
'bg-cyan-100/80',
'bg-orange-100/80',
'bg-lime-100/80',
'bg-indigo-100/80',
'bg-red-100/80',
'bg-sky-100/80',
'bg-emerald-100/80',
+28
View File
@@ -156,6 +156,34 @@
<span class="label-text">{{ t('required') }}</span>
</label>
</li>
<li
v-if="field.type == 'checkbox'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.default_value"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]"
>
<span class="label-text">{{ t('checked') }}</span>
</label>
</li>
<li
v-if="field.type == 'date'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.readonly"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]"
>
<span class="label-text">{{ t('set_signing_date') }}</span>
</label>
</li>
<li
v-if="field.type === 'text' && !defaultField"
@click.stop
@@ -67,7 +67,7 @@
</button>
</a>
</li>
<li v-if="submitters.length < 10 && editable">
<li v-if="submitters.length < names.length && editable">
<a
href="#"
class="flex px-2"
@@ -145,7 +145,7 @@
<a
href="#"
class="flex px-2 group justify-between items-center"
:class="{ 'active': submitter === selectedSubmitter }"
:class="{ 'active': submitter === selectedSubmitter, 'py-0.5': submitters.length > 8 }"
@click.prevent="selectSubmitter(submitter)"
>
<span class="py-1 flex items-center">
@@ -189,7 +189,7 @@
</div>
</a>
</li>
<li v-if="submitters.length < 10 && editable && allowAddNew">
<li v-if="submitters.length < names.length && editable && allowAddNew">
<a
href="#"
class="flex px-2"
@@ -268,6 +268,16 @@ export default {
computed: {
colors () {
return [
'bg-red-500',
'bg-sky-500',
'bg-emerald-500',
'bg-yellow-300',
'bg-purple-600',
'bg-pink-500',
'bg-cyan-500',
'bg-orange-500',
'bg-lime-500',
'bg-indigo-500',
'bg-red-500',
'bg-sky-500',
'bg-emerald-500',
@@ -291,7 +301,17 @@ export default {
this.t('seventh_party'),
this.t('eighth_party'),
this.t('ninth_party'),
this.t('tenth_party')
this.t('tenth_party'),
this.t('eleventh_party'),
this.t('twelfth_party'),
this.t('thirteenth_party'),
this.t('fourteenth_party'),
this.t('fifteenth_party'),
this.t('sixteenth_party'),
this.t('seventeenth_party'),
this.t('eighteenth_party'),
this.t('nineteenth_party'),
this.t('twentieth_party')
]
},
selectedSubmitter () {
+12
View File
@@ -1,7 +1,9 @@
const en = {
are_you_sure: 'Are you sure?',
sign_yourself: 'Sign Yourself',
set_signing_date: 'Set signing date',
send: 'Send',
checked: 'Checked',
save: 'Save',
cancel: 'Cancel',
draw_field_on_the_document: 'Draw {field} field on the document',
@@ -30,6 +32,16 @@ const en = {
eighth_party: 'Eighth Party',
ninth_party: 'Ninth Party',
tenth_party: 'Tenth Party',
eleventh_party: 'Eleventh Party',
twelfth_party: 'Twelfth Party',
thirteenth_party: 'Thirteenth Party',
fourteenth_party: 'Fourteenth Party',
fifteenth_party: 'Fifteenth Party',
sixteenth_party: 'Sixteenth Party',
seventeenth_party: 'Seventeenth Party',
eighteenth_party: 'Eighteenth Party',
nineteenth_party: 'Nineteenth Party',
twentieth_party: 'Twentieth Party',
add: 'Add',
or_add_field_without_drawing: 'Or add field without drawing',
text: 'Text',
@@ -3,6 +3,8 @@
class SendFormCompletedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook'
NotSuccessStatus = Class.new(StandardError)
def perform(submitter)
config = Accounts.load_webhook_configs(submitter.submission.account)
@@ -12,13 +14,17 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
Faraday.post(config.value,
{
event_type: 'form.completed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
resp = Faraday.post(config.value,
{
event_type: 'form.completed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
raise NotSuccessStatus, resp.status.to_s
end
end
end
@@ -3,6 +3,8 @@
class SendFormStartedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook'
NotSuccessStatus = Class.new(StandardError)
def perform(submitter)
config = Accounts.load_webhook_configs(submitter.submission.account)
@@ -10,13 +12,17 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
Faraday.post(config.value,
{
event_type: 'form.started',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
resp = Faraday.post(config.value,
{
event_type: 'form.started',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
raise NotSuccessStatus, resp.status.to_s
end
end
end
@@ -3,6 +3,8 @@
class SendFormViewedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook'
NotSuccessStatus = Class.new(StandardError)
def perform(submitter)
config = Accounts.load_webhook_configs(submitter.submission.account)
@@ -10,13 +12,17 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
Faraday.post(config.value,
{
event_type: 'form.viewed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
resp = Faraday.post(config.value,
{
event_type: 'form.viewed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
raise NotSuccessStatus, resp.status.to_s
end
end
end
+2 -1
View File
@@ -46,7 +46,8 @@ class SubmissionEvent < ApplicationRecord
phone_verified: 'phone_verified',
start_form: 'start_form',
view_form: 'view_form',
complete_form: 'complete_form'
complete_form: 'complete_form',
api_complete_form: 'api_complete_form'
}, scope: false
private
+19 -5
View File
@@ -121,12 +121,26 @@ Rails.application.configure do
config.active_record.dump_schema_after_migration = false
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Json.new
config.lograge.base_controller_class = ['ActionController::API', 'ActionController::Base']
config.lograge.custom_payload do |controller|
{
fwd: controller.request.ip
}
if ENV['MULTITENANT'] == 'true'
config.lograge.formatter = ->(data) { data.except(:path, :location).to_json }
config.lograge.custom_payload do |controller|
{
fwd: controller.request.ip.to_s[/\A\d+\.(.*)/, 1],
params: controller.request.params&.slice(:id),
host: controller.request.host,
uid: controller.instance_variable_get(:@current_user).try(:id)
}
end
else
config.lograge.formatter = Lograge::Formatters::Json.new
config.lograge.custom_payload do |controller|
{
fwd: controller.request.ip
}
end
end
end
+2 -1
View File
@@ -14,7 +14,8 @@ module SubmissionEvents
phone_verified: 'Phone verified',
start_form: 'Submission started',
view_form: 'Form viewed',
complete_form: 'Submission completed'
complete_form: 'Submission completed',
api_complete_form: 'Submission completed via API'
}.freeze
module_function
+1 -1
View File
@@ -66,7 +66,7 @@ module Submissions
first_submitter =
submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first
Submitters.send_signature_requests([first_submitter])
Submitters.send_signature_requests([first_submitter]) if first_submitter
else
Submitters.send_signature_requests(submitters)
end
+4 -2
View File
@@ -109,8 +109,10 @@ module Submissions
field['description'] = attrs['description'] if attrs['description'].present?
field['readonly'] = attrs['readonly'] if attrs.key?('readonly')
field['required'] = attrs['required'] if attrs.key?('required')
field['default_value'] = attrs['default_value'] if attrs['default_value'].present? &&
!field['type'].in?(%w[signature image initials file])
if attrs['default_value'].present? && !field['type'].in?(%w[signature image initials file])
field['default_value'] = Submitters::NormalizeValues.normalize_value(field, attrs['default_value'])
end
return field if attrs['validation_pattern'].blank?
+4 -2
View File
@@ -23,9 +23,11 @@ module Submissions
else
submitter.document_generation_events.create!(event_name: events.present? ? :retry : :start)
GenerateResultAttachments.call(submitter)
documents = GenerateResultAttachments.call(submitter)
submitter.document_generation_events.create!(event_name: :complete)
documents
end
rescue ActiveRecord::RecordNotUnique
sleep WAIT_FOR_RETRY
@@ -49,7 +51,7 @@ module Submissions
DocumentGenerationEvent.where(submitter:).order(:created_at).last
end
break last_event if last_event.event_name.in?(%w[complete fail])
break submitter.documents if last_event.event_name.in?(%w[complete fail])
raise WaitForCompleteTimeout if total_wait_time > CHECK_COMPLETE_TIMEOUT
end
+2
View File
@@ -236,6 +236,7 @@ module Submissions
end
value = value.join(', ') if value.is_a?(Array)
value = Submissions::GenerateResultAttachments::REPLACE_EMOJI[value] || value
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
@@ -278,6 +279,7 @@ module Submissions
sign_params = {
reason: SIGN_REASON,
certificate: pkcs.certificate,
doc_mdp_permissions: :no_changes,
key: pkcs.key,
certificate_chain: pkcs.ca_certs || []
}
@@ -21,6 +21,10 @@ module Submissions
A4_SIZE = [595, 842].freeze
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
REPLACE_EMOJI = {
'✔️' => 'V'
}.freeze
module_function
# rubocop:disable Metrics
@@ -173,6 +177,7 @@ module Submissions
end
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
value = REPLACE_EMOJI[value] || value
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
font_size:)
@@ -294,6 +299,12 @@ module Submissions
HexaPDF::Document.new(io: StringIO.new(attachment.download))
end
begin
pdf.acro_form&.flatten
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
end
[attachment.uuid, pdf]
end
end
+22 -2
View File
@@ -28,7 +28,9 @@ module Submitters
next if key.blank?
if fields_uuid_index[key]['type'].in?(%w[initials signature image file]) && value.present?
field = fields_uuid_index[key]
if field['type'].in?(%w[initials signature image file]) && value.present?
new_value, new_attachments = normalize_attachment_value(value, template.account, for_submitter)
attachments.push(*new_attachments)
@@ -36,12 +38,30 @@ module Submitters
value = new_value
end
[key, value]
[key, normalize_value(field, value)]
end.to_h
[normalized_values, attachments]
end
def normalize_value(field, value)
if field['type'] == 'text' && value.present?
value.to_s
elsif field['type'] == 'date' && value.present?
if value.is_a?(Integer)
Time.zone.at(value.to_s.first(10).to_i).to_date
else
Date.parse(value).to_s
end
else
value
end
rescue Date::Error => e
Rollbar.error(e) if defined?(Rollbar)
value
end
def fetch_fields(template, submitter_name: nil, for_submitter: nil)
if submitter_name
submitter =
+1
View File
@@ -103,6 +103,7 @@ module Submitters
end
def replace_default_variables(value, attrs, template, with_time: false)
return value if value.in?([true, false])
return if value.blank?
value.to_s.gsub(VARIABLE_REGEXP) do |e|
+1
View File
@@ -10,6 +10,7 @@ module Templates
pdf.pages.flat_map.with_index do |page, index|
annotations = page.objects.deref!(page.attributes[:Annots]) || []
annotations.filter_map do |annot|
next if annot.nil?
next if annot[:A].blank? || annot[:A][:URI].blank?
next unless annot[:Subtype] == :Link
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')