Compare commits

...
17 Commits
Author SHA1 Message Date
Pete Matsyburka 3034c00bb3 use sidekiq 2024-06-24 21:10:58 +03:00
Pete Matsyburka 9539c476c0 remove legacy tracking param 2024-06-24 14:33:33 +03:00
Pete Matsyburka 4552c72860 add index on external id 2024-06-24 13:27:14 +03:00
Pete Matsyburka 85f8a92721 optimize query 2024-06-23 12:20:01 +03:00
Pete Matsyburka 049b9c78c2 add retries 2024-06-23 09:16:34 +03:00
Pete Matsyburka acf06084bf simplify job params 2024-06-22 10:50:07 +03:00
Pete Matsyburka 61da477733 reorder 2024-06-22 10:50:07 +03:00
Pete Matsyburka fdeec548a3 adjust message 2024-06-22 10:50:07 +03:00
Pete Matsyburka ff0d486831 fix clone template folder 2024-06-22 10:50:07 +03:00
Pete Matsyburka d5bf54ee26 adjust tempalte preferences 2024-06-22 10:50:07 +03:00
Pete Matsyburka 7616774613 use latest alpine 2024-06-21 00:46:11 +03:00
Pete Matsyburka 27c4dc7eca font size in the UI 2024-06-20 22:15:45 +03:00
Pete Matsyburka 4ae834d6f7 skip optional signature 2024-06-20 17:52:40 +03:00
Pete Matsyburka b71a66a082 add font size preference 2024-06-20 16:14:18 +03:00
Pete Matsyburka 80cc38542c fix email attachments 2024-06-18 20:09:58 +03:00
Pete Matsyburka 185aecec42 increase signature size 2024-06-18 17:18:23 +03:00
Pete Matsyburka 8c092eba78 allow to permanently delete submissions 2024-06-18 16:54:15 +03:00
65 changed files with 524 additions and 197 deletions

No files matched your search

+1
View File
@@ -36,3 +36,4 @@ yarn-debug.log*
/attachments
/docuseal
/ee
dump.rdb
+3
View File
@@ -76,5 +76,8 @@ Rails/SkipsModelValidations:
Rails/ApplicationController:
Enabled: false
Rails/Output:
Enabled: false
Capybara/ClickLinkOrButtonStyle:
Enabled: false
+4 -4
View File
@@ -1,4 +1,4 @@
FROM ruby:3.3.1-alpine3.18 as fonts
FROM ruby:3.3.1-alpine as fonts
WORKDIR /fonts
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.3.1-alpine3.18 as webpack
FROM ruby:3.3.1-alpine as webpack
ENV RAILS_ENV=production
ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.3.1-alpine3.18 as app
FROM ruby:3.3.1-alpine as app
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
@@ -41,7 +41,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\
+2 -2
View File
@@ -27,7 +27,7 @@ gem 'pagy'
gem 'pg', require: false
gem 'premailer-rails'
gem 'pretender'
gem 'puma'
gem 'puma', require: false
gem 'rack'
gem 'rails'
gem 'rails_autolink'
@@ -37,7 +37,7 @@ gem 'rqrcode'
gem 'ruby-vips'
gem 'rubyXL'
gem 'shakapacker'
gem 'sidekiq', require: ENV.key?('REDIS_URL')
gem 'sidekiq'
gem 'sqlite3', require: false, force_ruby_platform: true
gem 'strip_attributes'
gem 'turbo-rails'
@@ -64,14 +64,14 @@ module Api
submissions = create_submissions(@template, params)
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_later(submission)
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions)
submissions.each do |submission|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
end
end
@@ -89,11 +89,15 @@ module Api
end
def destroy
@submission.update!(archived_at: Time.current)
if params[:permanently] == 'true'
@submission.destroy!
else
@submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
end
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
render json: @submission.as_json(only: %i[id archived_at])
end
private
@@ -8,8 +8,7 @@ module Api
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email') ||
params[:t] == SubmissionEvents.build_tracking_param_legacy(submitter, 'click_email')
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
end
@@ -13,7 +13,7 @@ module Api
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
SendFormViewedWebhookRequestJob.perform_later(submitter)
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
render json: {}
end
+1 -1
View File
@@ -66,7 +66,7 @@ module Api
end
if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_later(@submitter)
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter])
end
@@ -20,7 +20,7 @@ module Api
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
SendTemplateCreatedWebhookRequestJob.perform_later(cloned_template)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
render json: Templates::SerializeForApi.call(cloned_template)
end
+2 -2
View File
@@ -65,13 +65,13 @@ module Api
@template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
render json: @template.as_json(only: %i[id updated_at])
end
def destroy
if params[:permanently] == 'true' && !Docuseal.multitenant?
if params[:permanently] == 'true'
@template.destroy!
else
@template.update!(archived_at: Time.current)
+3 -1
View File
@@ -36,7 +36,9 @@ class StartFormController < ApplicationController
is_new_record = @submitter.new_record?
if @submitter.save
SendSubmissionCreatedWebhookRequestJob.perform_later(@submitter.submission) if is_new_record
if is_new_record
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
end
redirect_to submit_form_path(@submitter.slug)
else
+2 -2
View File
@@ -45,7 +45,7 @@ class SubmissionsController < ApplicationController
end
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_later(submission)
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions)
@@ -56,7 +56,7 @@ class SubmissionsController < ApplicationController
def destroy
@submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
end
@@ -13,7 +13,7 @@ class SubmittersSendEmailController < ApplicationController
alert: 'Email has been sent already.')
end
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
@submitter.sent_at ||= Time.current
@submitter.save!
+3 -3
View File
@@ -53,7 +53,7 @@ class TemplatesController < ApplicationController
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
@template.account_id = params[:account_id]
@template.folder = @template.account.default_template_folder
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
else
@template.account = current_account
end
@@ -61,7 +61,7 @@ class TemplatesController < ApplicationController
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
maybe_redirect_to_template(@template)
else
@@ -72,7 +72,7 @@ class TemplatesController < ApplicationController
def update
@template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
head :ok
end
@@ -21,7 +21,9 @@ class TemplatesPreferencesController < ApplicationController
params.require(:template).permit(
preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled]
documents_copy_email_enabled documents_copy_email_attach_audit
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit]
).tap do |attrs|
attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value)
@@ -18,7 +18,7 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:)
SendTemplateCreatedWebhookRequestJob.perform_later(@template)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted
@@ -17,7 +17,7 @@ class WebhookSettingsController < ApplicationController
def update
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
SendFormCompletedWebhookRequestJob.perform_later(submitter)
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
end
+6 -2
View File
@@ -5,8 +5,12 @@ export default targetable(class extends HTMLElement {
static [target.static] = ['canvas', 'input', 'clear', 'button']
async connectedCallback () {
this.canvas.width = this.canvas.parentNode.parentNode.clientWidth
this.canvas.height = this.canvas.parentNode.parentNode.clientWidth / 3
const scale = 3
this.canvas.width = this.canvas.parentNode.clientWidth * scale
this.canvas.height = this.canvas.parentNode.clientHeight * scale
this.canvas.getContext('2d').scale(scale, scale)
const { default: SignaturePad } = await import('signature_pad')
+7 -1
View File
@@ -340,12 +340,18 @@ export default {
computedStyle () {
const { x, y, w, h } = this.area
return {
const style = {
top: y * 100 + '%',
left: x * 100 + '%',
width: w * 100 + '%',
height: h * 100 + '%'
}
if (this.field.preferences?.font_size) {
style.fontSize = this.field.preferences.font_size + 'pt'
}
return style
}
},
watch: {
@@ -582,7 +582,11 @@ export default {
return resolve(attachment)
})
}).catch((error) => {
return reject(error)
if (error.message === 'Image too small' && this.field.required === false) {
return resolve({})
} else {
return reject(error)
}
})
})
}
+2 -2
View File
@@ -1136,7 +1136,7 @@ export default {
if (submitterWithoutFields) {
e.preventDefault()
alert(`Please add fields for ${submitterWithoutFields.name} to prepare the document or remove the party.`)
alert(`Please add fields for the ${submitterWithoutFields.name}. Or, remove the ${submitterWithoutFields.name} if not needed.`)
}
}
},
@@ -1158,7 +1158,7 @@ export default {
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
if (submitterWithoutFields) {
alert(`Please add fields for ${submitterWithoutFields.name} to prepare the document or remove the party.`)
alert(`Please add fields for the ${submitterWithoutFields.name}. Or, remove the ${submitterWithoutFields.name} if not needed.`)
} else {
this.isSaving = true
+1
View File
@@ -1,4 +1,5 @@
# frozen_string_literal: true
class ApplicationJob < ActiveJob::Base
retry_on StandardError, wait: 6.seconds, attempts: 5 unless Docuseal.multitenant?
end
+9 -4
View File
@@ -1,7 +1,11 @@
# frozen_string_literal: true
class ProcessSubmitterCompletionJob < ApplicationJob
def perform(submitter)
class ProcessSubmitterCompletionJob
include Sidekiq::Job
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil)
if !is_all_completed && submitter.submission.submitters_order_preserved?
@@ -18,7 +22,7 @@ class ProcessSubmitterCompletionJob < ApplicationJob
return if Accounts.load_webhook_url(submitter.account).blank?
SendFormCompletedWebhookRequestJob.perform_later(submitter)
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
end
def enqueue_completed_emails(submitter)
@@ -26,7 +30,8 @@ class ProcessSubmitterCompletionJob < ApplicationJob
user = submission.created_by_user || submitter.template.author
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false &&
submitter.template.preferences['completed_notification_email_enabled'] != false
if submission.submitters.map(&:email).exclude?(user.email) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false &&
user.role != 'integration'
@@ -1,14 +1,18 @@
# frozen_string_literal: true
class SendFormCompletedWebhookRequestJob < ApplicationJob
queue_as :webhooks
class SendFormCompletedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(submitter, params = {})
attempt = params[:attempt].to_i
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(submitter.submission.account)
return if url.blank?
@@ -36,11 +40,11 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormCompletedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(submitter, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
SendFormCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
@@ -1,14 +1,18 @@
# frozen_string_literal: true
class SendFormStartedWebhookRequestJob < ApplicationJob
queue_as :webhooks
class SendFormStartedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(submitter, params = {})
attempt = params[:attempt].to_i
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(submitter.submission.account)
return if url.blank?
@@ -34,11 +38,11 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormStartedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(submitter, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
@@ -1,14 +1,18 @@
# frozen_string_literal: true
class SendFormViewedWebhookRequestJob < ApplicationJob
queue_as :webhooks
class SendFormViewedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(submitter, params = {})
attempt = params[:attempt].to_i
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(submitter.submission.account)
return if url.blank?
@@ -34,11 +38,11 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormViewedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(submitter, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
@@ -1,14 +1,18 @@
# frozen_string_literal: true
class SendSubmissionArchivedWebhookRequestJob < ApplicationJob
queue_as :webhooks
class SendSubmissionArchivedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(submission, params = {})
attempt = params[:attempt].to_i
def perform(params = {})
submission = Submission.find(params['submission_id'])
attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(submission.account)
return if url.blank?
@@ -32,11 +36,11 @@ class SendSubmissionArchivedWebhookRequestJob < ApplicationJob
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionArchivedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(submission, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
@@ -1,14 +1,18 @@
# frozen_string_literal: true
class SendSubmissionCreatedWebhookRequestJob < ApplicationJob
queue_as :webhooks
class SendSubmissionCreatedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(submission, params = {})
attempt = params[:attempt].to_i
def perform(params = {})
submission = Submission.find(params['submission_id'])
attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(submission.account)
return if url.blank?
@@ -32,11 +36,11 @@ class SendSubmissionCreatedWebhookRequestJob < ApplicationJob
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionCreatedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(submission, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class SendSubmitterInvitationEmailJob < ApplicationJob
class SendSubmitterInvitationEmailJob
include Sidekiq::Job
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
@@ -1,14 +1,18 @@
# frozen_string_literal: true
class SendTemplateCreatedWebhookRequestJob < ApplicationJob
queue_as :webhooks
class SendTemplateCreatedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(template, params = {})
attempt = params[:attempt].to_i
def perform(params = {})
template = Template.find(params['template_id'])
attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(template.account)
return if url.blank?
@@ -32,11 +36,11 @@ class SendTemplateCreatedWebhookRequestJob < ApplicationJob
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateCreatedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(template, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
@@ -1,14 +1,18 @@
# frozen_string_literal: true
class SendTemplateUpdatedWebhookRequestJob < ApplicationJob
queue_as :webhooks
class SendTemplateUpdatedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(template, params = {})
attempt = params[:attempt].to_i
def perform(params = {})
template = Template.find(params['template_id'])
attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(template.account)
return if url.blank?
@@ -32,11 +36,11 @@ class SendTemplateUpdatedWebhookRequestJob < ApplicationJob
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateUpdatedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(template, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
+21 -9
View File
@@ -48,16 +48,21 @@ class SubmitterMailer < ApplicationMailer
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
add_completed_email_attachments!(
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
submitter, with_audit_log: (@email_config.nil? || @email_config.value['attach_audit_log'] != false) &&
@submitter.template.preferences['completed_notification_email_attach_audit'] != false
)
@subject = @submitter.template.preferences['completed_notification_email_subject'].presence
@subject ||= @email_config.value['subject'] if @email_config
@body = @submitter.template.preferences['completed_notification_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
subject =
if @email_config
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
if @subject.present?
ReplaceEmailVariables.call(@subject, submitter:)
else
submitters = submitter.submission.submitters.order(:completed_at)
.map { |e| e.name || e.email || e.phone }.join(', ')
%(#{submitter.submission.template.name} has been completed by #{submitters})
build_completed_subject(submitter)
end
assign_message_metadata('submitter_completed', @submitter)
@@ -77,7 +82,8 @@ class SubmitterMailer < ApplicationMailer
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
@documents = add_completed_email_attachments!(
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
submitter, with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false &&
(@email_config.nil? || @email_config.value['attach_audit_log'] != false)
)
@subject = @submitter.template.preferences['documents_copy_email_subject'].presence
@@ -105,6 +111,12 @@ class SubmitterMailer < ApplicationMailer
private
def build_completed_subject(submitter)
submitters = submitter.submission.submitters.order(:completed_at)
.map { |e| e.name || e.email || e.phone }.join(', ')
%(#{submitter.submission.template.name} has been completed by #{submitters})
end
def add_completed_email_attachments!(submitter, with_audit_log: true)
documents = Submitters.select_attachments_for_download(submitter)
@@ -119,7 +131,7 @@ class SubmitterMailer < ApplicationMailer
total_size = add_attachments_with_size_limit(documents, total_size)
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
attachments[submitter.submission.audit_trail.filename.to_s.tr('"', "'")] = audit_trail_data if audit_trail_data
file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) }
@@ -141,7 +153,7 @@ class SubmitterMailer < ApplicationMailer
break if total_size >= MAX_ATTACHMENTS_SIZE
attachments[attachment.filename.to_s] = attachment.download
attachments[attachment.filename.to_s.tr('"', "'")] = attachment.download
end
total_size
+1
View File
@@ -26,6 +26,7 @@
# Indexes
#
# index_submitters_on_email (email)
# index_submitters_on_external_id (external_id)
# index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id)
#
+5 -4
View File
@@ -22,10 +22,11 @@
#
# Indexes
#
# index_templates_on_account_id (account_id)
# index_templates_on_author_id (author_id)
# index_templates_on_folder_id (folder_id)
# index_templates_on_slug (slug) UNIQUE
# index_templates_on_account_id (account_id)
# index_templates_on_author_id (author_id)
# index_templates_on_external_id (external_id)
# index_templates_on_folder_id (folder_id)
# index_templates_on_slug (slug) UNIQUE
#
# Foreign Keys
#
+6
View File
@@ -81,6 +81,12 @@ class User < ApplicationRecord
true
end
def sidekiq?
return true if Rails.env.development?
role == 'admin'
end
def self.sign_in_after_reset_password
if PasswordsController::Current.user.present?
!PasswordsController::Current.user.otp_required_for_login
@@ -4,7 +4,7 @@
<%= f.label :value, class: 'label' do %>
<span class="flex items-center space-x-1">
<span>
Completed documents BCC address
Completed documents notification BCC address
</span>
<span class="tooltip" data-tip="Send email copy with completed documents to a specified BCC address.">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
@@ -2,7 +2,7 @@
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
Form Completed Email
Completed Notification Email
</div>
</div>
<div class="collapse-content">
@@ -4,8 +4,8 @@
<p class="text-4xl font-bold mb-4">Email Templates</p>
<div class="space-y-4">
<%= render 'signature_request_email_form' %>
<%= render 'submitter_completed_email_form' %>
<%= render 'documents_copy_email_form' %>
<%= render 'submitter_completed_email_form' %>
</div>
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
<%= render 'logo_form' %>
+1 -1
View File
@@ -1,5 +1,5 @@
<% align = field.dig('preferences', 'align') %>
<field-value dir="auto" class="flex absolute text-[1.5vw] lg:text-base <%= 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 %>%">
<field-value dir="auto" class="flex absolute text-[1.5vw] lg:text-base <%= 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') %>">
<% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'].in?(['file', 'payment']) %>
@@ -1,5 +1,5 @@
<% if @email_config %>
<%= render 'custom_content', content: @email_config.value['body'], submitter: @submitter %>
<% if @body.present? %>
<%= render 'custom_content', content: @body, submitter: @submitter %>
<% else %>
<p>Hi there,</p>
<p>"<%= @submitter.submission.template.name %>" form has been completed by <%= @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.join(', ') %>.</p>
+56 -2
View File
@@ -24,7 +24,7 @@
<%= ff.label :bcc_completed, class: 'label' do %>
<span class="flex items-center space-x-1 justify-between w-full">
<span>
Completed documents BCC address
Completed documents notification BCC address
</span>
</span>
<% end %>
@@ -82,7 +82,8 @@
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled).new(*(@template.preferences.values_at('documents_copy_email_subject', 'documents_copy_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value.values_at('subject', 'body')), @template.preferences['documents_copy_email_enabled']) do |ff| %>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit).new(@template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false) do |ff| %>
<div class="form-control">
<%= ff.label :documents_copy_email_subject, 'Email subject', class: 'label' %>
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
@@ -98,6 +99,12 @@
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
Attach Audit Log PDF to the email
</span>
<%= ff.check_box :documents_copy_email_attach_audit, { checked: ff.object.documents_copy_email_attach_audit != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
Send emails automatically on completion
@@ -114,6 +121,53 @@
<% end %>
</div>
</div>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
Completed notification email
</div>
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body, :completed_notification_email_enabled, :completed_notification_email_attach_audit).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body'], @template.preferences['completed_notification_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['completed_notification_email_attach_audit'] != false) do |ff| %>
<div class="form-control">
<%= ff.label :completed_notification_email_subject, 'Email subject', class: 'label' %>
<%= ff.text_field :completed_notification_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :completed_notification_email_body, 'Email body', class: 'label' %>
<span class="tooltip tooltip-right" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :completed_notification_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
Attach Audit Log PDF to the email
</span>
<%= ff.check_box :completed_notification_email_attach_audit, { checked: ff.object.completed_notification_email_attach_audit != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
Send emails automatically on completion
</span>
<%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert3" class="text-sm invisible font-normal mt-0.5">Changes have been saved</span>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<% if show_api %>
+1 -1
View File
@@ -16,7 +16,7 @@
<%= form_for @user_config, url: user_initials_path, method: :put, data: { turbo_frame: :_top }, html: { autocomplete: :off, enctype: 'multipart/form-data' } do |f| %>
<signature-form class="relative block">
<a class="absolute top-1 right-1 link text-sm" data-target="signature-form.clear" href="#">Clear</a>
<canvas data-target="signature-form.canvas" class="bg-white border border-base-300 rounded"></canvas>
<canvas data-target="signature-form.canvas" class="bg-white border border-base-300 w-full rounded"></canvas>
<input name="file" class="hidden" data-target="signature-form.input" type="file" accept="image/png,image/jpeg,image/jpg">
<div class="form-control mt-4">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button', data: { target: 'signature-form.button' } %>
+1 -1
View File
@@ -16,7 +16,7 @@
<%= form_for @user_config, url: user_signature_path, method: :put, data: { turbo_frame: :_top }, html: { autocomplete: :off, enctype: 'multipart/form-data' } do |f| %>
<signature-form class="relative block">
<a class="absolute top-1 right-1 link text-sm" data-target="signature-form.clear" href="#">Clear</a>
<canvas data-target="signature-form.canvas" class="bg-white border border-base-300 rounded"></canvas>
<canvas data-target="signature-form.canvas" class="bg-white border border-base-300 w-full rounded"></canvas>
<input name="file" class="hidden" data-target="signature-form.input" type="file" accept="image/png,image/jpeg,image/jpg">
<div class="form-control mt-4">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button', data: { target: 'signature-form.button' } %>
+1 -1
View File
@@ -19,7 +19,7 @@ module DocuSeal
class Application < Rails::Application
config.load_defaults 7.1
config.autoload_lib(ignore: %w[assets tasks])
config.autoload_lib(ignore: %w[assets tasks puma])
config.active_storage.routes_prefix = ''
+1 -35
View File
@@ -2,41 +2,7 @@
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
if ENV['RAILS_ENV'] == 'production' && ENV['SECRET_KEY_BASE'].to_s.empty?
require 'dotenv'
require 'securerandom'
dotenv_path = "#{ENV.fetch('WORKDIR', '.')}/docuseal.env"
unless File.exist?(dotenv_path)
default_env = <<~TEXT
DATABASE_URL= # keep empty to use sqlite or specify postgresql database URL
SECRET_KEY_BASE=#{SecureRandom.hex(64)}
TEXT
File.write(dotenv_path, default_env)
end
database_url = ENV.fetch('DATABASE_URL', nil)
Dotenv.load(dotenv_path)
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
end
if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_')
require 'addressable'
url = Addressable::URI.parse(ENV.fetch('DATABASE_URL', ''))
ENV['DATABASE_HOST'] = url.host
ENV['DATABASE_PORT'] = (url.port || 5432).to_s
ENV['DATABASE_USER'] = url.user
ENV['DATABASE_PASSWORD'] = url.password
ENV['DATABASE_NAME'] = url.path.to_s.delete_prefix('/')
ENV.delete('DATABASE_URL')
end
require_relative 'dotenv'
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+4 -2
View File
@@ -1,7 +1,7 @@
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15) %>
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
development:
<<: *default
@@ -22,12 +22,14 @@ production:
<% elsif ENV['DATABASE_URL'].to_s.empty? %>
adapter: sqlite3
database: <%= ENV['WORKDIR'] || '.' %>/db.sqlite3
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
timeout: 5000
<% elsif ENV['DATABASE_URL'].match?(/\Apostgres/) %>
<<: *default
url: <%= ENV['DATABASE_URL'] %>
<% elsif ENV['DATABASE_URL'].match?(/\Amysql/) %>
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15) %>
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
url: <%= ENV['DATABASE_URL'] %>
<% end %>
+46
View File
@@ -0,0 +1,46 @@
# frozen_string_literal: true
if ENV['RAILS_ENV'] == 'production' && ENV['SECRET_KEY_BASE'].to_s.empty?
require 'dotenv'
require 'securerandom'
dotenv_path = "#{ENV.fetch('WORKDIR', '.')}/docuseal.env"
unless File.exist?(dotenv_path)
default_env = <<~TEXT
DATABASE_URL= # keep empty to use sqlite or specify postgresql database URL
SECRET_KEY_BASE=#{SecureRandom.hex(64)}
TEXT
File.write(dotenv_path, default_env)
end
database_url = ENV.fetch('DATABASE_URL', nil)
Dotenv.load(dotenv_path)
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
end
if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_')
require 'addressable'
url = Addressable::URI.parse(ENV.fetch('DATABASE_URL', ''))
ENV['DATABASE_HOST'] = url.host
ENV['DATABASE_PORT'] = (url.port || 5432).to_s
ENV['DATABASE_USER'] = url.user
ENV['DATABASE_PASSWORD'] = url.password
ENV['DATABASE_NAME'] = url.path.to_s.delete_prefix('/')
ENV.delete('DATABASE_URL')
end
if ENV['REDIS_URL'].to_s.empty?
require 'digest'
redis_password = Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}")
ENV['REDIS_URL'] = "redis://default:#{redis_password}@0.0.0.0:6379/0"
ENV['LOCAL_REDIS_URL'] = ENV.fetch('REDIS_URL', nil)
end
+1 -1
View File
@@ -47,7 +47,7 @@ Rails.application.configure do
config.cache_store = :null_store
end
config.active_job.queue_adapter = :sidekiq if defined?(Sidekiq)
config.active_job.queue_adapter = :sidekiq
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :disk
+1 -1
View File
@@ -27,7 +27,7 @@ Rails.application.configure do
config.action_controller.perform_caching = true
config.active_record.sqlite3_production_warning = false
config.active_job.queue_adapter = :sidekiq if defined?(Sidekiq)
config.active_job.queue_adapter = :sidekiq
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
+1 -1
View File
@@ -44,7 +44,7 @@ Rails.application.configure do
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test
config.active_job.queue_adapter = :sidekiq
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
+4 -6
View File
@@ -1,16 +1,14 @@
# frozen_string_literal: true
if defined?(Sidekiq)
require 'sidekiq/web'
require 'sidekiq/web' if defined?(Puma)
if !ENV['SIDEKIQ_BASIC_AUTH_PASSWORD'].to_s.empty? && defined?(Sidekiq::Web)
Sidekiq::Web.use(Rack::Auth::Basic) do |_, password|
next true if Rails.env.development?
ActiveSupport::SecurityUtils.secure_compare(
Digest::SHA256.hexdigest(password),
Digest::SHA256.hexdigest(ENV.fetch('SIDEKIQ_BASIC_AUTH_PASSWORD'))
)
end
Sidekiq.strict_args!
end
Sidekiq.strict_args!
+10 -3
View File
@@ -5,7 +5,9 @@
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
require_relative 'dotenv'
max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 15)
min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
threads min_threads_count, max_threads_count
@@ -41,5 +43,10 @@ workers ENV.fetch('WEB_CONCURRENCY', 0)
#
# preload_app!
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
if ENV['MULTITENANT'] != 'true' || ENV['DEMO'] == 'true'
require_relative '../lib/puma/plugin/redis_server'
require_relative '../lib/puma/plugin/sidekiq_embed'
plugin :sidekiq_embed
plugin :redis_server
end
+6 -1
View File
@@ -2,7 +2,12 @@
Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
mount Sidekiq::Web => '/sidekiq' if defined?(Sidekiq)
if !Docuseal.multitenant? && defined?(Sidekiq::Web)
authenticated :user, ->(u) { u.sidekiq? } do
mount Sidekiq::Web => '/jobs'
end
end
root 'dashboard#index'
@@ -0,0 +1,8 @@
# frozen_string_literal: true
class AddIndexOnExternalId < ActiveRecord::Migration[7.1]
def change
add_index :submitters, :external_id
add_index :templates, :external_id
end
end
+3 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_06_04_070648) do
ActiveRecord::Schema[7.1].define(version: 2024_06_24_102526) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -200,6 +200,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_04_070648) do
t.text "preferences", null: false
t.text "metadata", null: false
t.index ["email"], name: "index_submitters_on_email"
t.index ["external_id"], name: "index_submitters_on_external_id"
t.index ["slug"], name: "index_submitters_on_slug", unique: true
t.index ["submission_id"], name: "index_submitters_on_submission_id"
end
@@ -242,6 +243,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_04_070648) do
t.text "preferences", null: false
t.index ["account_id"], name: "index_templates_on_account_id"
t.index ["author_id"], name: "index_templates_on_author_id"
t.index ["external_id"], name: "index_templates_on_external_id"
t.index ["folder_id"], name: "index_templates_on_folder_id"
t.index ["slug"], name: "index_templates_on_slug", unique: true
end
+11 -6
View File
@@ -5,13 +5,18 @@ module Abilities
module_function
def collection(user, ability: nil)
shared_ids =
Template.joins(:template_sharings)
.where(template_sharings: { ability:,
account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact)
.select(:id)
template_ids = Template.where(account_id: user.account_id).select(:id)
Template.where(account_id: user.account_id).or(Template.where(id: shared_ids))
shared_ids =
TemplateSharing.where({ ability:,
account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact)
.select(:template_id)
join_query = Template.arel_table
.join(template_ids.arel.union(shared_ids.arel).as('union_ids'))
.on(Template.arel_table[:id].eq(Arel::Table.new(:union_ids)[:id]))
Template.joins(join_query.join_sources.first)
end
def entity(template, user:, ability: nil)
+70
View File
@@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'puma/plugin'
# rubocop:disable Metrics
Puma::Plugin.create do
def start(launcher)
return if ENV['LOCAL_REDIS_URL'].to_s.empty?
@puma_pid = $PROCESS_ID
launcher.events.on_booted do
@redis_server_pid = fork_redis
end
in_background { monitor_redis }
at_exit do
stop_redis_server if Process.pid == @puma_pid
end
launcher.events.on_stopped { stop_redis_server }
launcher.events.on_restart { stop_redis_server }
end
private
def monitor_redis
loop do
if redis_dead?
Process.kill(:INT, @puma_pid)
break
end
sleep 5
end
end
def redis_dead?
return false unless @redis_server_pid
Process.waitpid(@redis_server_pid, Process::WNOHANG)
false
rescue Errno::ECHILD, Errno::ESRCH
true
end
def fork_redis
fork do
Process.setsid
Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty?
exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"),
out: '/dev/null')
end
end
def stop_redis_server
if @redis_server_pid
Process.kill(:INT, @redis_server_pid)
Process.wait(@redis_server_pid)
end
rescue Errno::ECHILD, Errno::ESRCH
nil
end
end
# rubocop:enable Metrics
+74
View File
@@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'puma/plugin'
# rubocop:disable Metrics
Puma::Plugin.create do
def config(cfg)
return if cfg.instance_variable_get(:@options)[:workers] <= 0
cfg.on_worker_boot { start_sidekiq! }
cfg.on_worker_shutdown { @sidekiq&.stop }
cfg.on_refork { @sidekiq&.stop }
end
def start(launcher)
launcher.events.on_booted do
next if Puma.stats_hash[:workers].to_i != 0
start_sidekiq!
end
launcher.events.on_stopped { Thread.new { @sidekiq&.stop }.join }
launcher.events.on_restart { Thread.new { @sidekiq&.stop }.join }
end
def fire_event(config, event)
arr = config[:lifecycle_events][event]
arr.each(&:call)
arr.clear
end
def start_sidekiq!
Thread.new do
wait_for_redis!
configs = Sidekiq.configure_embed do |config|
config.logger.level = Logger::INFO
sidekiq_config = YAML.load_file('config/sidekiq.yml')
config.queues = sidekiq_config['queues']
config.concurrency = ENV.fetch('SIDEKIQ_THREADS', 5).to_i
config.merge!(sidekiq_config)
config[:max_retries] = 13
ActiveSupport.run_load_hooks(:sidekiq_config, config)
end.instance_variable_get(:@config)
@sidekiq = Sidekiq::Launcher.new(configs, embedded: true)
@sidekiq.run
fire_event(configs, :startup)
end
end
def wait_for_redis!
attempt = 0
loop do
attempt += 1
sleep (attempt - 1) / 10.0
RedisClient.new(url: ENV.fetch('REDIS_URL', nil)).call('GET', '1')
break
rescue RedisClient::CannotConnectError
raise('Unable to connect to redis') if attempt > 10
end
end
end
# rubocop:enable Metrics
-6
View File
@@ -20,12 +20,6 @@ module SubmissionEvents
module_function
def build_tracking_param_legacy(submitter, event_type = 'click_email')
Base64.urlsafe_encode64(
[submitter.slug, event_type].join(':')
).first(TRACKING_PARAM_LENGTH)
end
def build_tracking_param(submitter, event_type = 'click_email')
Base64.urlsafe_encode64(
Digest::SHA1.digest([submitter.slug, event_type, Rails.application.secret_key_base].join(':'))
@@ -120,7 +120,11 @@ module Submissions
width = page.box.width
height = page.box.height
font_size = (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
preferences_font_size = field.dig('preferences', 'font_size').then { |num| num.present? ? num.to_i : nil }
font_size = preferences_font_size
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
value = submitter.values[field['uuid']]
@@ -265,7 +269,7 @@ module Submissions
lines = layouter.fit([text], area['w'] * width, height).lines
box_height = lines.sum(&:height)
if box_height > (area['h'] * height) + 1
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value,
font: pdf.fonts.add(FONT_NAME),
font_size: (font_size / 1.4).to_i)
@@ -275,7 +279,7 @@ module Submissions
box_height = lines.sum(&:height)
end
if box_height > (area['h'] * height) + 1
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value,
font: pdf.fonts.add(FONT_NAME),
font_size: (font_size / 1.9).to_i)
+1 -1
View File
@@ -73,7 +73,7 @@ module Submitters
next if submitter.email.blank?
next if submitter.preferences['send_email'] == false
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => submitter.id)
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
end
end
end
+2 -2
View File
@@ -14,14 +14,14 @@ module Submitters
unless submitter.submission_events.exists?(event_type: 'start_form')
SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request)
SendFormStartedWebhookRequestJob.perform_later(submitter)
SendFormStartedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
end
update_submitter!(submitter, params, request)
submitter.submission.save!
ProcessSubmitterCompletionJob.perform_later(submitter) if submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at?
submitter
end
+11
View File
@@ -9,6 +9,9 @@ require 'rspec/rails'
require 'capybara/cuprite'
require 'capybara/rspec'
require 'webmock/rspec'
require 'sidekiq/testing'
Sidekiq::Testing.fake!
WebMock.disable_net_connect!(allow_localhost: true)
@@ -56,4 +59,12 @@ RSpec.configure do |config|
driven_by :headless_cuprite
end
end
config.before do
Sidekiq::Worker.clear_all
end
config.before do |example|
Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline
end
end
+1 -1
View File
@@ -14,7 +14,7 @@ RSpec.describe 'Personalization' do
it 'shows the personalization page' do
expect(page).to have_content('Email Templates')
expect(page).to have_content('Signature Request Email')
expect(page).to have_content('Form Completed Email')
expect(page).to have_content('Completed Notification Email')
expect(page).to have_content('Documents Copy Email')
expect(page).to have_content('Company Logo')
expect(page).to have_content('Unlock with DocuSeal Pro')
+1 -1
View File
@@ -81,7 +81,7 @@ RSpec.describe 'Submit Form' do
expect do
click_on 'submit'
end.to change(enqueued_jobs, :size).by(2)
end.to change(enqueued_jobs, :size).by(1)
end
end
end