mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27a0d10525 | ||
|
|
d25f0812ca | ||
|
|
9e2aa6f8c8 | ||
|
|
11c76bf554 | ||
|
|
e5733d88c0 | ||
|
|
6d38c9f375 | ||
|
|
91ec29ad2c | ||
|
|
d374c7fa10 | ||
|
|
ca0daf6d0b | ||
|
|
076c3d805a | ||
|
|
1f149ac67a | ||
|
|
616f7b4fd8 | ||
|
|
6f4527038b | ||
|
|
8ed2883504 | ||
|
|
651b84a460 | ||
|
|
73c4f158ff | ||
|
|
6f1cc34433 | ||
|
|
a77029ffcb | ||
|
|
0480572ef5 | ||
|
|
807463c465 | ||
|
|
0f61053976 | ||
|
|
b8cf525034 | ||
|
|
fef203daed | ||
|
|
41b38cc594 | ||
|
|
4e4354af99 | ||
|
|
113b61588b | ||
|
|
c1aac6b763 | ||
|
|
f85fd3cff7 |
No files matched your search
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
|
|||||||
rescue_from RateLimit::LimitApproached do |e|
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
redirect_back_or_to root_path, alert: 'Too many requests'
|
||||||
end
|
end
|
||||||
|
|
||||||
if Rails.env.production? || Rails.env.test?
|
if Rails.env.production? || Rails.env.test?
|
||||||
|
|||||||
@@ -8,13 +8,9 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
around_action :with_browser_locale, only: %i[show update completed]
|
around_action :with_browser_locale, only: %i[show update completed]
|
||||||
before_action :maybe_redirect_com, only: %i[show completed]
|
before_action :maybe_redirect_com, only: %i[show completed]
|
||||||
before_action :load_resubmit_submitter, only: :update
|
|
||||||
before_action :load_template
|
before_action :load_template
|
||||||
before_action :authorize_start!, only: :update
|
before_action :authorize_start!, only: :update
|
||||||
|
|
||||||
COOKIES_TTL = 12.hours
|
|
||||||
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
|
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
|
||||||
raise ActionController::RoutingError, I18n.t('not_found')
|
raise ActionController::RoutingError, I18n.t('not_found')
|
||||||
@@ -23,8 +19,7 @@ class StartFormController < ApplicationController
|
|||||||
if @template.shared_link?
|
if @template.shared_link?
|
||||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
.submitters.new(account_id: @template.account_id,
|
.submitters.new(account_id: @template.account_id,
|
||||||
uuid: (filter_undefined_submitters(@template).first ||
|
uuid: Submitters::StartForm.first_submitter_uuid(@template))
|
||||||
@template.submitters.first)['uuid'])
|
|
||||||
render :email_verification if params[:email_verification]
|
render :email_verification if params[:email_verification]
|
||||||
else
|
else
|
||||||
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||||
@@ -41,24 +36,28 @@ class StartFormController < ApplicationController
|
|||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
|
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
|
||||||
else
|
else
|
||||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
|
||||||
@error_message = multiple_submitters_error_message
|
@error_message = multiple_submitters_error_message
|
||||||
|
|
||||||
return render :show, status: :unprocessable_content
|
return render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if (is_new_record = @submitter.new_record?)
|
if (is_new_record = @submitter.new_record?)
|
||||||
assign_submission_attributes(@submitter, @template)
|
Submitters::StartForm.assign_submission_attributes(
|
||||||
|
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent
|
||||||
|
)
|
||||||
|
|
||||||
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||||
else
|
else
|
||||||
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @template.preferences['shared_link_2fa'] == true
|
if require_link_2fa?(@template, @submitter)
|
||||||
handle_require_2fa(@submitter, is_new_record:)
|
handle_require_2fa(@template, @submitter)
|
||||||
elsif @submitter.errors.blank? && @submitter.save
|
elsif @submitter.errors.blank? && @submitter.save
|
||||||
enqueue_new_submitter_jobs(@submitter) if is_new_record
|
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||||
|
|
||||||
|
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
|
||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
else
|
else
|
||||||
@@ -88,41 +87,40 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def enqueue_new_submitter_jobs(submitter)
|
def find_or_initialize_submitter(template, submitter_params)
|
||||||
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
|
required_fields = template.preferences.fetch('link_form_fields', ['email'])
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(submitter)
|
blank_fields = required_fields.select { |key| submitter_params[key].blank? }
|
||||||
|
|
||||||
expire_at = submitter.submission.expire_at
|
submitter =
|
||||||
|
if blank_fields.present?
|
||||||
return unless expire_at
|
Submitter.new(submitter_params)
|
||||||
|
else
|
||||||
ProcessSubmissionExpiredJob.perform_at(expire_at, 'submission_id' => submitter.submission_id,
|
Submitters::StartForm.find_or_initialize_submitter(
|
||||||
'expire_at' => expire_at.to_i)
|
template, submitter_params, exclude_completed: params[:resubmit].present?,
|
||||||
end
|
request:, current_user:
|
||||||
|
)
|
||||||
def load_resubmit_submitter
|
|
||||||
@resubmit_submitter =
|
|
||||||
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
|
||||||
submitter = Submitter.find_by(slug: params[:resubmit])
|
|
||||||
|
|
||||||
submitter if submitter && can_resubmit?(submitter)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
blank_fields.each { |key| submitter.errors.add(key.to_sym, :blank) }
|
||||||
|
|
||||||
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_resubmit?(submitter)
|
def require_link_2fa?(template, submitter)
|
||||||
submitter.completed_at? && submitter.completed_at > 14.days.ago &&
|
return true if template.preferences['shared_link_2fa'] == true
|
||||||
%w[api embed mcp].exclude?(submitter.submission.source) &&
|
return false if cookies.encrypted[:start_form_slug] == submitter.slug
|
||||||
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
|
return false if current_user && submitter.email == current_user.email &&
|
||||||
|
current_user.account_id == submitter.account_id
|
||||||
|
|
||||||
|
!submitter.new_record?
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorize_start!
|
def authorize_start!
|
||||||
is_archived = @template.archived_at? || @template.account.archived_at?
|
is_archived = @template.archived_at? || @template.account.archived_at?
|
||||||
|
|
||||||
return redirect_to submit_form_path(@resubmit_submitter.slug) if @resubmit_submitter && is_archived
|
|
||||||
return redirect_to start_form_path(@template.slug) if is_archived
|
return redirect_to start_form_path(@template.slug) if is_archived
|
||||||
|
|
||||||
return if @resubmit_submitter
|
|
||||||
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
|
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
|
||||||
|
|
||||||
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||||
@@ -130,90 +128,14 @@ class StartFormController < ApplicationController
|
|||||||
redirect_to start_form_path(@template.slug)
|
redirect_to start_form_path(@template.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_initialize_submitter(template, submitter_params)
|
|
||||||
required_fields = template.preferences.fetch('link_form_fields', ['email'])
|
|
||||||
|
|
||||||
required_params = required_fields.index_with { |key| submitter_params[key] }
|
|
||||||
|
|
||||||
find_params = required_params.except('name')
|
|
||||||
|
|
||||||
submitter = Submitter.new if find_params.compact_blank.blank?
|
|
||||||
|
|
||||||
submitter ||=
|
|
||||||
Submitter
|
|
||||||
.where(submission: template.submissions.non_expired.active)
|
|
||||||
.order(id: :desc)
|
|
||||||
.where(declined_at: nil)
|
|
||||||
.where(external_id: nil)
|
|
||||||
.where(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
|
|
||||||
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
|
||||||
.find_or_initialize_by(find_params)
|
|
||||||
|
|
||||||
submitter = Submitter.new(find_params) if submitter.submission&.completed_at? && submitter.viewer?
|
|
||||||
|
|
||||||
submitter.name = required_params['name'] if submitter.new_record?
|
|
||||||
|
|
||||||
unless @resubmit_submitter
|
|
||||||
required_params.each do |key, value|
|
|
||||||
submitter.errors.add(key.to_sym, :blank) if value.blank?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
submitter
|
|
||||||
end
|
|
||||||
|
|
||||||
def assign_submission_attributes(submitter, template)
|
|
||||||
submitter.assign_attributes(
|
|
||||||
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
|
|
||||||
ip: request.remote_ip,
|
|
||||||
ua: request.user_agent,
|
|
||||||
values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
|
||||||
preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
|
||||||
metadata: @resubmit_submitter&.metadata.presence || {}
|
|
||||||
)
|
|
||||||
|
|
||||||
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
|
|
||||||
|
|
||||||
if submitter.values.present?
|
|
||||||
@resubmit_submitter.attachments.each do |attachment|
|
|
||||||
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
submitter.submission ||= Submission.new(template:,
|
|
||||||
account_id: template.account_id,
|
|
||||||
template_submitters: template.submitters,
|
|
||||||
expire_at: Templates.build_default_expire_at(template),
|
|
||||||
submitters: [submitter],
|
|
||||||
source: :link)
|
|
||||||
|
|
||||||
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission)
|
|
||||||
|
|
||||||
submitter.account_id = submitter.submission.account_id
|
|
||||||
|
|
||||||
submitter
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_undefined_submitters(template)
|
|
||||||
Templates.filter_undefined_submitters(template.submitters)
|
|
||||||
end
|
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
return { 'email' => current_user.email, 'name' => current_user.full_name } if params[:selfsign]
|
|
||||||
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
|
|
||||||
|
|
||||||
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
@template =
|
@template = Template.find_by!(slug: params[:slug] || params[:start_form_slug])
|
||||||
if @resubmit_submitter
|
|
||||||
@resubmit_submitter.template
|
|
||||||
else
|
|
||||||
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def multiple_submitters_error_message
|
def multiple_submitters_error_message
|
||||||
@@ -224,38 +146,27 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_require_2fa(submitter, is_new_record:)
|
def handle_require_2fa(template, submitter)
|
||||||
return render :show, status: :unprocessable_content if submitter.errors.present?
|
return render :show, status: :unprocessable_content if submitter.errors.present?
|
||||||
|
|
||||||
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
|
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request, is_new_record: submitter.new_record?)
|
||||||
|
redirect_to submit_form_path(submitter.slug)
|
||||||
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
|
|
||||||
if submitter.save
|
|
||||||
enqueue_new_submitter_jobs(submitter) if is_new_record
|
|
||||||
|
|
||||||
if is_otp_verified
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
|
|
||||||
|
|
||||||
cookies.encrypted[:email_2fa_slug] =
|
|
||||||
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect_to submit_form_path(submitter.slug)
|
|
||||||
else
|
|
||||||
render :show, status: :unprocessable_content
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
|
if defined?(Rollbar) && template.preferences['shared_link_2fa'] != true
|
||||||
|
Rollbar.info("2FA link requested: #{submitter.id}")
|
||||||
|
end
|
||||||
|
|
||||||
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
||||||
|
|
||||||
render :email_verification
|
render :email_verification
|
||||||
end
|
end
|
||||||
|
rescue Submitters::StartForm::NotSaved
|
||||||
|
render :show, status: :unprocessable_content
|
||||||
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
|
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
|
||||||
redirect_to start_form_path(submitter.submission.template.slug,
|
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
|
||||||
params: submitter_params.merge(email_verification: true)),
|
|
||||||
alert: e.message
|
alert: e.message
|
||||||
rescue RateLimit::LimitApproached
|
rescue RateLimit::LimitApproached
|
||||||
redirect_to start_form_path(submitter.submission.template.slug,
|
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
|
||||||
params: submitter_params.merge(email_verification: true)),
|
|
||||||
alert: I18n.t(:too_many_attempts)
|
alert: I18n.t(:too_many_attempts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -13,6 +13,10 @@ class StartFormEmail2faSendController < ApplicationController
|
|||||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
.submitters.new(**submitter_params, account_id: @template.account_id)
|
.submitters.new(**submitter_params, account_id: @template.account_id)
|
||||||
|
|
||||||
|
if @submitter.email.blank?
|
||||||
|
return redirect_to start_form_path(@template.slug), alert: I18n.t(:provide_your_email_to_start)
|
||||||
|
end
|
||||||
|
|
||||||
Submitters.send_shared_link_email_verification_code(@submitter, request:)
|
Submitters.send_shared_link_email_verification_code(@submitter, request:)
|
||||||
|
|
||||||
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
|
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
|
||||||
@@ -36,14 +40,24 @@ class StartFormEmail2faSendController < ApplicationController
|
|||||||
return redirect_to start_form_path(@template.slug) if is_archived
|
return redirect_to start_form_path(@template.slug) if is_archived
|
||||||
|
|
||||||
return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) &&
|
return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) &&
|
||||||
@template.preferences['shared_link_2fa'] == true
|
(@template.preferences['shared_link_2fa'] == true || submitter_exists?(@template, params))
|
||||||
|
|
||||||
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
Rollbar.warning("Not 2FA shared template: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
redirect_to start_form_path(@template.slug)
|
redirect_to start_form_path(@template.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def submitter_exists?(template, params)
|
||||||
|
email = Submissions.normalize_email(params.dig(:submitter, :email))
|
||||||
|
|
||||||
|
return false if email.blank?
|
||||||
|
|
||||||
|
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true).exists?(email:)
|
||||||
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
params.require(:submitter).permit(:name, :email, :phone)
|
params.require(:submitter).permit(:name, :email, :phone).tap do |attrs|
|
||||||
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StartFormResubmitController < ApplicationController
|
||||||
|
layout 'form'
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
around_action :with_browser_locale
|
||||||
|
before_action :load_resubmit_submitter
|
||||||
|
before_action :load_template
|
||||||
|
before_action :authorize_start!
|
||||||
|
|
||||||
|
def update
|
||||||
|
@submitter = find_or_initialize_submitter(@template, @resubmit_submitter)
|
||||||
|
|
||||||
|
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
|
||||||
|
@error_message = multiple_submitters_error_message
|
||||||
|
|
||||||
|
return render 'start_form/show', status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
if (is_new_record = @submitter.new_record?)
|
||||||
|
Submitters::StartForm.assign_submission_attributes(
|
||||||
|
@submitter, @template,
|
||||||
|
ip: request.remote_ip, user_agent: request.user_agent, resubmit_submitter: @resubmit_submitter,
|
||||||
|
source: @resubmit_submitter.submission.source_self? ? :self : :link
|
||||||
|
)
|
||||||
|
|
||||||
|
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||||
|
else
|
||||||
|
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @template.preferences['shared_link_2fa'] == true
|
||||||
|
handle_require_2fa(@submitter)
|
||||||
|
elsif @submitter.save
|
||||||
|
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||||
|
|
||||||
|
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
|
||||||
|
|
||||||
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
|
else
|
||||||
|
render 'start_form/show', status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_or_initialize_submitter(template, resubmit_submitter)
|
||||||
|
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true)
|
||||||
|
.find_by(slug: cookies.encrypted[:start_form_slug],
|
||||||
|
email: resubmit_submitter.email) || Submitter.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_resubmit_submitter
|
||||||
|
@resubmit_submitter = Submitter.find_by(slug: params[:resubmit])
|
||||||
|
|
||||||
|
raise ActiveRecord::RecordNotFound if @resubmit_submitter.blank? ||
|
||||||
|
!Submitters::StartForm.can_resubmit?(@resubmit_submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_template
|
||||||
|
@template = @resubmit_submitter.template
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_start!
|
||||||
|
redirect_to submit_form_path(@resubmit_submitter.slug) if @template.archived_at? || @template.account.archived_at?
|
||||||
|
end
|
||||||
|
|
||||||
|
def multiple_submitters_error_message
|
||||||
|
if current_user&.account_id == @template.account_id
|
||||||
|
helpers.t('this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html')
|
||||||
|
else
|
||||||
|
I18n.t('not_found')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_require_2fa(submitter)
|
||||||
|
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request,
|
||||||
|
is_new_record: submitter.new_record?)
|
||||||
|
redirect_to submit_form_path(submitter.slug)
|
||||||
|
else
|
||||||
|
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
||||||
|
|
||||||
|
render 'start_form/email_verification'
|
||||||
|
end
|
||||||
|
rescue Submitters::StartForm::NotSaved
|
||||||
|
render 'start_form/show', status: :unprocessable_content
|
||||||
|
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
|
||||||
|
flash.now[:alert] = e.message
|
||||||
|
|
||||||
|
render 'start_form/email_verification'
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
flash.now[:alert] = I18n.t(:too_many_attempts)
|
||||||
|
|
||||||
|
render 'start_form/email_verification'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StartFormSelfController < ApplicationController
|
||||||
|
load_resource :template, parent: false
|
||||||
|
|
||||||
|
around_action :with_browser_locale
|
||||||
|
before_action :authorize_start!
|
||||||
|
|
||||||
|
def update
|
||||||
|
@submitter = Submitters::StartForm.build_submitters_scope(@template, exclude_completed: true, source: :self)
|
||||||
|
.find_or_initialize_by(email: current_user.email)
|
||||||
|
|
||||||
|
@submitter = Submitter.new(email: current_user.email) if @submitter.persisted? &&
|
||||||
|
outdated_template_fields?(@submitter, @template)
|
||||||
|
|
||||||
|
if (is_new_record = @submitter.new_record?)
|
||||||
|
@submitter.name = current_user.full_name
|
||||||
|
|
||||||
|
Submitters::StartForm.assign_submission_attributes(
|
||||||
|
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent, source: :self
|
||||||
|
)
|
||||||
|
|
||||||
|
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||||
|
else
|
||||||
|
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||||
|
end
|
||||||
|
|
||||||
|
@submitter.save!
|
||||||
|
|
||||||
|
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||||
|
|
||||||
|
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
|
||||||
|
|
||||||
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def outdated_template_fields?(submitter, template)
|
||||||
|
template_fields = submitter.submission.template_fields
|
||||||
|
|
||||||
|
template_fields.present? && template_fields != template.fields
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_start!
|
||||||
|
authorize!(:read, @template)
|
||||||
|
|
||||||
|
redirect_to template_path(@template) if @template.archived_at?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -19,7 +19,7 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
request)
|
request)
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
@submitter.update!(declined_at: Time.current)
|
@submitter.update!(declined_at: Time.current, values: @submitter.preferences['default_values'] || {})
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] })
|
SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] })
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ class SubmitFormEmail2fasController < ApplicationController
|
|||||||
|
|
||||||
before_action :load_submitter
|
before_action :load_submitter
|
||||||
|
|
||||||
COOKIES_TTL = 12.hours
|
|
||||||
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
@@ -20,7 +17,8 @@ class SubmitFormEmail2fasController < ApplicationController
|
|||||||
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
||||||
|
|
||||||
cookies.encrypted[:email_2fa_slug] =
|
cookies.encrypted[:email_2fa_slug] =
|
||||||
{ value: @submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
{ value: @submitter.slug, expires: Submitters::StartForm::COOKIES_TTL.from_now,
|
||||||
|
**Submitters::StartForm::COOKIES_DEFAULTS }
|
||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -560,6 +560,7 @@
|
|||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
:values="values"
|
:values="values"
|
||||||
:readonly-values="readonlyFieldValues"
|
:readonly-values="readonlyFieldValues"
|
||||||
|
:fetch-options="fetchOptions"
|
||||||
:provider="paymentProvider"
|
:provider="paymentProvider"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@@ -573,6 +574,7 @@
|
|||||||
:empty-value-required-step="emptyValueRequiredStep"
|
:empty-value-required-step="emptyValueRequiredStep"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
|
:fetch-options="fetchOptions"
|
||||||
:values="values"
|
:values="values"
|
||||||
@submit="!isSubmitting && submitStep()"
|
@submit="!isSubmitting && submitStep()"
|
||||||
/>
|
/>
|
||||||
@@ -585,6 +587,7 @@
|
|||||||
:empty-value-required-step="emptyValueRequiredStep"
|
:empty-value-required-step="emptyValueRequiredStep"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
|
:fetch-options="fetchOptions"
|
||||||
:values="values"
|
:values="values"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="!isSubmitting && submitStep()"
|
@submit="!isSubmitting && submitStep()"
|
||||||
@@ -1776,7 +1779,7 @@ export default {
|
|||||||
window.location.href = sanitizeUrl(this.completedRedirectUrl)
|
window.location.href = sanitizeUrl(this.completedRedirectUrl)
|
||||||
} else {
|
} else {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode?.getRootNode() || document
|
||||||
const completedEl = root.getElementById('form_completed')
|
const completedEl = root.getElementById('form_completed')
|
||||||
|
|
||||||
if (completedEl) {
|
if (completedEl) {
|
||||||
|
|||||||
@@ -312,6 +312,11 @@ export default {
|
|||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['submit'],
|
emits: ['submit'],
|
||||||
@@ -494,8 +499,9 @@ export default {
|
|||||||
|
|
||||||
const resp = await fetch(this.baseUrl + '/api/kba', {
|
const resp = await fetch(this.baseUrl + '/api/kba', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify(payload),
|
||||||
body: JSON.stringify(payload)
|
...this.fetchOptions,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
@@ -550,7 +556,8 @@ export default {
|
|||||||
reference: this.reference,
|
reference: this.reference,
|
||||||
submitter_slug: this.submitterSlug
|
submitter_slug: this.submitterSlug
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
...this.fetchOptions,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['focus', 'submit', 'update:model-value', 'attached'],
|
emits: ['focus', 'submit', 'update:model-value', 'attached'],
|
||||||
@@ -234,7 +239,8 @@ export default {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug
|
submitter_slug: this.submitterSlug
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
...this.fetchOptions,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
if (resp.status === 422 || resp.status === 500) {
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
@@ -274,7 +280,8 @@ export default {
|
|||||||
check_status: checkStatus,
|
check_status: checkStatus,
|
||||||
referer: this.normalizedPaymentUrl()
|
referer: this.normalizedPaymentUrl()
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
...this.fetchOptions,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
if (resp.status === 422 || resp.status === 500) {
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ export default {
|
|||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['focus', 'submit', 'update:model-value', 'attached'],
|
emits: ['focus', 'submit', 'update:model-value', 'attached'],
|
||||||
@@ -163,7 +168,8 @@ export default {
|
|||||||
submitter_slug: this.submitterSlug,
|
submitter_slug: this.submitterSlug,
|
||||||
redirect_url: document.location.href
|
redirect_url: document.location.href
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
...this.fetchOptions,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
this.eidEasyData = await resp.json()
|
this.eidEasyData = await resp.json()
|
||||||
|
|
||||||
@@ -205,7 +211,8 @@ export default {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug
|
submitter_slug: this.submitterSlug
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
...this.fetchOptions,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (resp.status === 404) {
|
if (resp.status === 404) {
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
data-turbo="false"
|
data-turbo="false"
|
||||||
class="inline"
|
class="inline"
|
||||||
method="post"
|
method="post"
|
||||||
:action="`/d/${template.slug}`"
|
:action="`/start_form_self/${template.id}`"
|
||||||
@submit="maybeShowErrorTemplateAlert"
|
@submit="maybeShowErrorTemplateAlert"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -125,12 +125,6 @@
|
|||||||
:value="authenticityToken"
|
:value="authenticityToken"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="selfsign"
|
|
||||||
value="true"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class GeneratePreviewImagesJob
|
|
||||||
include Sidekiq::Job
|
|
||||||
|
|
||||||
sidekiq_options queue: :images
|
|
||||||
|
|
||||||
def perform(params = {})
|
|
||||||
attachment = ActiveStorage::Attachment.find(params['attachment_id'])
|
|
||||||
|
|
||||||
max_page = [attachment.metadata['pdf']['number_of_pages'].to_i - 1,
|
|
||||||
Templates::ProcessDocument::MAX_NUMBER_OF_PAGES_PROCESSED].min
|
|
||||||
|
|
||||||
Templates::ProcessDocument.generate_document_preview_images(attachment, attachment.download, 1..max_page,
|
|
||||||
concurrency: 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -112,7 +112,8 @@ class Submission < ApplicationRecord
|
|||||||
api: 'api',
|
api: 'api',
|
||||||
embed: 'embed',
|
embed: 'embed',
|
||||||
mcp: 'mcp',
|
mcp: 'mcp',
|
||||||
link: 'link'
|
link: 'link',
|
||||||
|
self: 'self'
|
||||||
}, scope: false, prefix: true
|
}, scope: false, prefix: true
|
||||||
|
|
||||||
enum :submitters_order, {
|
enum :submitters_order, {
|
||||||
|
|||||||
@@ -21,13 +21,15 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'bcc_form', config: @bcc_config %>
|
<%= render 'bcc_form', config: @bcc_config %>
|
||||||
<div class="flex justify-between items-end mb-4 mt-8">
|
<% unless can?(:manage, :personalization_restricted) %>
|
||||||
<h2 class="text-2xl md:text-3xl font-bold">
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
<%= t('sign_request_email_reminders') %>
|
<h2 class="text-2xl md:text-3xl font-bold">
|
||||||
</h2>
|
<%= t('sign_request_email_reminders') %>
|
||||||
</div>
|
</h2>
|
||||||
<%= render 'reminder_banner' %>
|
</div>
|
||||||
<%= render 'reminder_form', config: @reminder_config %>
|
<%= render 'reminder_banner' %>
|
||||||
|
<%= render 'reminder_form', config: @reminder_config %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -10,7 +10,12 @@
|
|||||||
<%= f.hidden_field :key %>
|
<%= f.hidden_field :key %>
|
||||||
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %>
|
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
|
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
|
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
|
||||||
|
|||||||
@@ -10,7 +10,12 @@
|
|||||||
<%= f.hidden_field :key %>
|
<%= f.hidden_field :key %>
|
||||||
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %>
|
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
|
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
|
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||||
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
|
<span class="tooltip tooltip-right" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %> <%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<%= render 'banner' %>
|
<%= render 'start_form/banner' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %>
|
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %>
|
||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed mcp].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %>
|
<% if @template.shared_link? && Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed mcp].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
|
||||||
<toggle-submit class="block">
|
<toggle-submit class="block">
|
||||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: params.permit(:name, :email, :phone).compact_blank, resubmit: true }, method: :put, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: params.permit(:name, :email, :phone).compact_blank, resubmit: true }, method: :put, class: 'white-button w-full' %>
|
||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
|
|||||||
@@ -23,12 +23,12 @@
|
|||||||
<div>
|
<div>
|
||||||
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
|
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
|
||||||
</div>
|
</div>
|
||||||
<%= form_for '', url: start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
|
<%= form_for '', url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
|
||||||
<div dir="auto" class="form-control !mt-0">
|
<div dir="auto" class="form-control !mt-0">
|
||||||
<%= f.hidden_field 'resubmit', value: params[:resubmit] %>
|
<%= f.hidden_field 'resubmit', value: params[:resubmit] %>
|
||||||
<%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %>
|
<%= f.hidden_field 'submitter[name]', value: params[:name] || params.dig(:submitter, :name) %>
|
||||||
<%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %>
|
<%= f.hidden_field 'submitter[email]', value: params[:email] || params.dig(:submitter, :email) %>
|
||||||
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %>
|
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || params.dig(:submitter, :phone) %>
|
||||||
<%= f.text_field :one_time_code, required: true, inputmode: 'numeric', class: 'base-input text-center', placeholder: 'XXXXXX' %>
|
<%= f.text_field :one_time_code, required: true, inputmode: 'numeric', class: 'base-input text-center', placeholder: 'XXXXXX' %>
|
||||||
<div class="flex justify-between items-center mt-1">
|
<div class="flex justify-between items-center mt-1">
|
||||||
<span>
|
<span>
|
||||||
@@ -49,6 +49,10 @@
|
|||||||
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
|
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
|
||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || @submitter&.name, email: params[:email] || @submitter&.email, phone: params[:phone] || @submitter&.phone } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
|
<% if @resubmit_submitter %>
|
||||||
|
<%= button_to t(:re_send_email), resubmit_form_path, params: { resubmit: @resubmit_submitter.slug }, method: :put, id: 'resend_code', class: 'hidden' %>
|
||||||
|
<% else %>
|
||||||
|
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
<main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
||||||
<div class="text-center w-full space-y-6">
|
<div class="text-center w-full space-y-6">
|
||||||
<%= render 'banner' %>
|
<%= render 'start_form/banner' %>
|
||||||
<p class="text-xl font-semibold text-center">
|
<p class="text-xl font-semibold text-center">
|
||||||
<%= t('share_link_is_currently_disabled') %>
|
<%= t('share_link_is_currently_disabled') %>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="text-center w-full space-y-6">
|
<div class="text-center w-full space-y-6">
|
||||||
<%= render 'banner' %>
|
<%= render 'start_form/banner' %>
|
||||||
<% unless @template.archived_at? %>
|
<% unless @template.archived_at? %>
|
||||||
<p class="text-xl font-semibold text-center"><%= t('you_have_been_invited_to_submit_a_form') %></p>
|
<p class="text-xl font-semibold text-center"><%= t('you_have_been_invited_to_submit_a_form') %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -28,7 +28,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if !@template.archived_at? && !@template.account.archived_at? %>
|
<% if !@template.archived_at? && !@template.account.archived_at? %>
|
||||||
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
|
<%= form_for @submitter, url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
|
||||||
|
<%= hidden_field_tag :resubmit, @resubmit_submitter.slug if @resubmit_submitter %>
|
||||||
<% if @error_message %>
|
<% if @error_message %>
|
||||||
<div class="alert items-start bg-base-100 border border-red-400 text-red-400">
|
<div class="alert items-start bg-base-100 border border-red-400 text-red-400">
|
||||||
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
|
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template_submitters) %>
|
<% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template_submitters) %>
|
||||||
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && @submitter.completed_at? && @submitter.completed_at > 14.days.ago && %w[api embed mcp].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @submitter.template && !@submitter.template.archived_at? %>
|
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && Submitters::StartForm.can_resubmit?(@submitter) && @submitter.template && !@submitter.template.archived_at? %>
|
||||||
<div class="divider uppercase"><%= t('or') %></div>
|
<div class="divider uppercase"><%= t('or') %></div>
|
||||||
<toggle-submit class="block">
|
<toggle-submit class="block">
|
||||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), resubmit_form_path, params: { resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), resubmit_form_path, params: { resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
</open-modal-mobile>
|
</open-modal-mobile>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && @template.variables_schema.blank? %>
|
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && @template.variables_schema.blank? %>
|
||||||
<%= button_to start_form_path(@template.slug), params: { selfsign: true }, method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %>
|
<%= button_to start_form_self_path(@template), method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %>
|
||||||
<%= svg_icon('writing', class: 'w-6 h-6') %>
|
<%= svg_icon('writing', class: 'w-6 h-6') %>
|
||||||
<span class="mr-1"><%= t('sign_it_yourself') %></span>
|
<span class="mr-1"><%= t('sign_it_yourself') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -10,7 +10,12 @@
|
|||||||
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body']) do |ff| %>
|
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body']) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<% if is_custom_template_email %>
|
<% if is_custom_template_email %>
|
||||||
<label for="submitter_completed_email_reset_link" class="label underline">
|
<label for="submitter_completed_email_reset_link" class="label underline">
|
||||||
<%= t('reset_default') %>
|
<%= t('reset_default') %>
|
||||||
|
|||||||
@@ -10,7 +10,12 @@
|
|||||||
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :documents_copy_email_subject, :documents_copy_email_body).new(@template.preferences['documents_copy_email_reply_to'].presence || configs['reply_to'], @template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body']) do |ff| %>
|
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :documents_copy_email_subject, :documents_copy_email_body).new(@template.preferences['documents_copy_email_reply_to'].presence || configs['reply_to'], @template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body']) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<% if is_custom_template_email %>
|
<% if is_custom_template_email %>
|
||||||
<label for="submitter_documents_copy_email_reset_link" class="label underline">
|
<label for="submitter_documents_copy_email_reset_link" class="label underline">
|
||||||
<%= t('reset_default') %>
|
<%= t('reset_default') %>
|
||||||
|
|||||||
@@ -16,7 +16,12 @@
|
|||||||
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(template_email_preferences_values.presence || default_template_email_preferences_values)) do |ff| %>
|
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(template_email_preferences_values.presence || default_template_email_preferences_values)) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<% if is_custom_template_email %>
|
<% if is_custom_template_email %>
|
||||||
<label for="submitter_invitation_email_reset_link" class="label underline">
|
<label for="submitter_invitation_email_reset_link" class="label underline">
|
||||||
<%= t('reset_default') %>
|
<%= t('reset_default') %>
|
||||||
|
|||||||
@@ -10,7 +10,12 @@
|
|||||||
<%= f.fields_for :preferences, Struct.new(:invitation_view_email_subject, :invitation_view_email_body).new(@template.preferences['invitation_view_email_subject'].presence || configs['subject'], @template.preferences['invitation_view_email_body'].presence || configs['body']) do |ff| %>
|
<%= f.fields_for :preferences, Struct.new(:invitation_view_email_subject, :invitation_view_email_body).new(@template.preferences['invitation_view_email_subject'].presence || configs['subject'], @template.preferences['invitation_view_email_body'].presence || configs['body']) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<%= ff.label :invitation_view_email_subject, t('email_subject'), class: 'label' %>
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :invitation_view_email_subject, t('email_subject'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('view_only_email_sent_to_recipients_that_need_to_only_review_the_documents') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<% if is_custom_template_email %>
|
<% if is_custom_template_email %>
|
||||||
<label for="submitter_view_invitation_email_reset_link" class="label underline">
|
<label for="submitter_view_invitation_email_reset_link" class="label underline">
|
||||||
<%= t('reset_default') %>
|
<%= t('reset_default') %>
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ module HexaPDF
|
|||||||
end
|
end
|
||||||
|
|
||||||
module CycleSafeInheritedValue
|
module CycleSafeInheritedValue
|
||||||
|
using HexaPDF::Type::AcroForm::Field::HashRefinement
|
||||||
|
|
||||||
def inherited_value(field, name)
|
def inherited_value(field, name)
|
||||||
seen = Set.new.compare_by_identity
|
seen = Set.new.compare_by_identity
|
||||||
seen << field.value
|
seen << field.value
|
||||||
|
|||||||
@@ -992,6 +992,13 @@ en: &en
|
|||||||
open_file_from: 'Open file from'
|
open_file_from: 'Open file from'
|
||||||
cancel: Cancel
|
cancel: Cancel
|
||||||
open: Open
|
open: Open
|
||||||
|
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: Signature request email sent to recipients with a link to review and sign.
|
||||||
|
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: View only email sent to recipients that need to only review the documents.
|
||||||
|
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Reminds recipients who haven't completed their part yet to sign, following the reminders schedule.
|
||||||
|
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: Signature request SMS sent to recipients with a link to review and sign.
|
||||||
|
phone_2fa_verification_text_message: Phone 2FA verification text message.
|
||||||
|
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Notification email sent to the sender user and BCC addresses once the submission is completed by all parties.
|
||||||
|
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: Completed documents copy email sent to recipients upon completion containing their documents.
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Your email address has been successfully confirmed.
|
confirmed: Your email address has been successfully confirmed.
|
||||||
@@ -1007,6 +1014,7 @@ en: &en
|
|||||||
invite: Invite
|
invite: Invite
|
||||||
link: Link
|
link: Link
|
||||||
mcp: MCP
|
mcp: MCP
|
||||||
|
self: Sign Yourself
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
||||||
bounce_email_html: '<b>Email bounced</b> %{submitter_name}'
|
bounce_email_html: '<b>Email bounced</b> %{submitter_name}'
|
||||||
@@ -1470,6 +1478,13 @@ es: &es
|
|||||||
completed_form_message: Mensaje de formulario completado
|
completed_form_message: Mensaje de formulario completado
|
||||||
completed_form_redirect_button: Botón de redirección de formulario completado
|
completed_form_redirect_button: Botón de redirección de formulario completado
|
||||||
documents_copy_email: Correo de copia de documentos
|
documents_copy_email: Correo de copia de documentos
|
||||||
|
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: Correo de solicitud de firma enviado a los destinatarios con un enlace para revisar y firmar.
|
||||||
|
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: Correo de solo lectura enviado a los destinatarios que solo necesitan revisar los documentos.
|
||||||
|
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Recuerda firmar a los destinatarios que aún no han completado su parte, según la programación de recordatorios.
|
||||||
|
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS de solicitud de firma enviado a los destinatarios con un enlace para revisar y firmar.
|
||||||
|
phone_2fa_verification_text_message: Mensaje de texto de verificación 2FA por teléfono.
|
||||||
|
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Correo de notificación enviado al usuario remitente y a las direcciones BCC una vez que todas las partes han completado el envío.
|
||||||
|
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: Correo con la copia de los documentos completados enviado a los destinatarios al finalizar, con sus documentos.
|
||||||
form_custom_css: CSS personalizado del formulario
|
form_custom_css: CSS personalizado del formulario
|
||||||
subject: Asunto
|
subject: Asunto
|
||||||
body: Cuerpo
|
body: Cuerpo
|
||||||
@@ -2118,6 +2133,7 @@ es: &es
|
|||||||
invite: Invitación
|
invite: Invitación
|
||||||
link: Enlace
|
link: Enlace
|
||||||
mcp: MCP
|
mcp: MCP
|
||||||
|
self: Firma tú mismo
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
||||||
bounce_email_html: '<b>Correo electrónico rebotado</b> %{submitter_name}'
|
bounce_email_html: '<b>Correo electrónico rebotado</b> %{submitter_name}'
|
||||||
@@ -2595,6 +2611,13 @@ it: &it
|
|||||||
completed_form_message: Messaggio di completamento del modulo
|
completed_form_message: Messaggio di completamento del modulo
|
||||||
completed_form_redirect_button: Pulsante di reindirizzamento dopo il completamento del modulo
|
completed_form_redirect_button: Pulsante di reindirizzamento dopo il completamento del modulo
|
||||||
documents_copy_email: Email con copia dei documenti
|
documents_copy_email: Email con copia dei documenti
|
||||||
|
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: Email di richiesta di firma inviata ai destinatari con un link per revisionare e firmare.
|
||||||
|
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: Email di sola lettura inviata ai destinatari che devono solo revisionare i documenti.
|
||||||
|
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Ricorda di firmare ai destinatari che non hanno ancora completato la loro parte, in base alla pianificazione dei promemoria.
|
||||||
|
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS di richiesta di firma inviato ai destinatari con un link per revisionare e firmare.
|
||||||
|
phone_2fa_verification_text_message: Messaggio di testo per la verifica 2FA tramite telefono.
|
||||||
|
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Email di notifica inviata all'utente mittente e agli indirizzi BCC una volta che l'invio è stato completato da tutte le parti.
|
||||||
|
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: Email con la copia dei documenti completati inviata ai destinatari al completamento, con i loro documenti.
|
||||||
form_custom_css: CSS personalizzato del modulo
|
form_custom_css: CSS personalizzato del modulo
|
||||||
subject: Oggetto
|
subject: Oggetto
|
||||||
body: Corpo
|
body: Corpo
|
||||||
@@ -3243,6 +3266,7 @@ it: &it
|
|||||||
invite: Invito
|
invite: Invito
|
||||||
link: Link
|
link: Link
|
||||||
mcp: MCP
|
mcp: MCP
|
||||||
|
self: Firma te stesso
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Email inviata</b> a %{submitter_name}'
|
send_email_to_html: '<b>Email inviata</b> a %{submitter_name}'
|
||||||
bounce_email_html: '<b>Email respinta</b> %{submitter_name}'
|
bounce_email_html: '<b>Email respinta</b> %{submitter_name}'
|
||||||
@@ -3706,6 +3730,13 @@ fr: &fr
|
|||||||
completed_form_message: Message de formulaire complété
|
completed_form_message: Message de formulaire complété
|
||||||
completed_form_redirect_button: Bouton de redirection après finalisation
|
completed_form_redirect_button: Bouton de redirection après finalisation
|
||||||
documents_copy_email: E‑mail avec copie des documents
|
documents_copy_email: E‑mail avec copie des documents
|
||||||
|
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: E-mail de demande de signature envoyé aux destinataires avec un lien pour consulter et signer.
|
||||||
|
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: E-mail en lecture seule envoyé aux destinataires qui doivent uniquement consulter les documents.
|
||||||
|
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Rappelle de signer aux destinataires qui n'ont pas encore terminé leur partie, selon la planification des rappels.
|
||||||
|
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS de demande de signature envoyé aux destinataires avec un lien pour consulter et signer.
|
||||||
|
phone_2fa_verification_text_message: Message texte de vérification 2FA par téléphone.
|
||||||
|
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: E-mail de notification envoyé à l'utilisateur expéditeur et aux adresses Cci une fois la soumission complétée par toutes les parties.
|
||||||
|
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: E-mail avec la copie des documents finalisés, envoyé aux destinataires à la finalisation, contenant leurs documents.
|
||||||
form_custom_css: CSS personnalisé du formulaire
|
form_custom_css: CSS personnalisé du formulaire
|
||||||
subject: Objet
|
subject: Objet
|
||||||
body: Corps
|
body: Corps
|
||||||
@@ -4351,6 +4382,7 @@ fr: &fr
|
|||||||
invite: Invitation
|
invite: Invitation
|
||||||
link: Lien
|
link: Lien
|
||||||
mcp: MCP
|
mcp: MCP
|
||||||
|
self: Signer vous-même
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: "<b>E‑mail envoyé</b> à %{submitter_name}"
|
send_email_to_html: "<b>E‑mail envoyé</b> à %{submitter_name}"
|
||||||
bounce_email_html: "<b>E‑mail rejeté</b> %{submitter_name}"
|
bounce_email_html: "<b>E‑mail rejeté</b> %{submitter_name}"
|
||||||
@@ -4827,6 +4859,13 @@ pt: &pt
|
|||||||
completed_form_message: Mensagem de formulário concluído
|
completed_form_message: Mensagem de formulário concluído
|
||||||
completed_form_redirect_button: Botão de redirecionamento do formulário concluído
|
completed_form_redirect_button: Botão de redirecionamento do formulário concluído
|
||||||
documents_copy_email: E-mail com cópia dos documentos
|
documents_copy_email: E-mail com cópia dos documentos
|
||||||
|
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: E-mail de solicitação de assinatura enviado aos destinatários com um link para revisar e assinar.
|
||||||
|
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: E-mail somente de leitura enviado aos destinatários que precisam apenas revisar os documentos.
|
||||||
|
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Lembra os destinatários que ainda não concluíram a sua parte de assinar, de acordo com o agendamento de lembretes.
|
||||||
|
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS de solicitação de assinatura enviado aos destinatários com um link para revisar e assinar.
|
||||||
|
phone_2fa_verification_text_message: Mensagem de texto de verificação 2FA por telefone.
|
||||||
|
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: E-mail de notificação enviado ao usuário remetente e aos endereços BCC assim que a submissão for concluída por todas as partes.
|
||||||
|
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: E-mail com a cópia dos documentos concluídos enviado aos destinatários na conclusão, com os seus documentos.
|
||||||
form_custom_css: CSS personalizado do formulário
|
form_custom_css: CSS personalizado do formulário
|
||||||
subject: Assunto
|
subject: Assunto
|
||||||
body: Corpo
|
body: Corpo
|
||||||
@@ -5475,6 +5514,7 @@ pt: &pt
|
|||||||
invite: Convite
|
invite: Convite
|
||||||
link: Link
|
link: Link
|
||||||
mcp: MCP
|
mcp: MCP
|
||||||
|
self: Assine você mesmo
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Email enviado</b> para %{submitter_name}'
|
send_email_to_html: '<b>Email enviado</b> para %{submitter_name}'
|
||||||
bounce_email_html: '<b>Email não entregue</b> %{submitter_name}'
|
bounce_email_html: '<b>Email não entregue</b> %{submitter_name}'
|
||||||
@@ -5951,6 +5991,13 @@ de: &de
|
|||||||
completed_form_message: Nachricht zum abgeschlossenen Formular
|
completed_form_message: Nachricht zum abgeschlossenen Formular
|
||||||
completed_form_redirect_button: Weiterleitungsschaltfläche nach Formularabschluss
|
completed_form_redirect_button: Weiterleitungsschaltfläche nach Formularabschluss
|
||||||
documents_copy_email: E-Mail mit Dokumentenkopie
|
documents_copy_email: E-Mail mit Dokumentenkopie
|
||||||
|
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: E-Mail mit der Signaturanfrage, die mit einem Link zum Prüfen und Signieren an die Empfänger gesendet wird.
|
||||||
|
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: E-Mail nur zur Ansicht, die an Empfänger gesendet wird, die die Dokumente nur prüfen müssen.
|
||||||
|
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Erinnert Empfänger, die ihren Teil noch nicht abgeschlossen haben, an das Signieren, gemäß dem Erinnerungszeitplan.
|
||||||
|
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS mit der Signaturanfrage, die mit einem Link zum Prüfen und Signieren an die Empfänger gesendet wird.
|
||||||
|
phone_2fa_verification_text_message: Textnachricht zur 2FA-Verifizierung per Telefon.
|
||||||
|
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Benachrichtigungs-E-Mail, die an den absendenden Benutzer und die BCC-Adressen gesendet wird, sobald die Einreichung von allen Parteien abgeschlossen wurde.
|
||||||
|
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: E-Mail mit der Kopie der abgeschlossenen Dokumente, die nach Abschluss an die Empfänger gesendet wird.
|
||||||
form_custom_css: Benutzerdefiniertes CSS für Formular
|
form_custom_css: Benutzerdefiniertes CSS für Formular
|
||||||
subject: Betreff
|
subject: Betreff
|
||||||
body: Inhalt
|
body: Inhalt
|
||||||
@@ -6599,6 +6646,7 @@ de: &de
|
|||||||
invite: Einladung
|
invite: Einladung
|
||||||
link: Link
|
link: Link
|
||||||
mcp: MCP
|
mcp: MCP
|
||||||
|
self: Selbst unterschreiben
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
||||||
bounce_email_html: '<b>E-Mail unzustellbar</b> %{submitter_name}'
|
bounce_email_html: '<b>E-Mail unzustellbar</b> %{submitter_name}'
|
||||||
@@ -7595,6 +7643,13 @@ nl: &nl
|
|||||||
completed_form_message: Bericht voltooid formulier
|
completed_form_message: Bericht voltooid formulier
|
||||||
completed_form_redirect_button: Knop voor doorverwijzing na voltooid formulier
|
completed_form_redirect_button: Knop voor doorverwijzing na voltooid formulier
|
||||||
documents_copy_email: E-mail met kopie van documenten
|
documents_copy_email: E-mail met kopie van documenten
|
||||||
|
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: E-mail met het handtekeningverzoek die naar de ontvangers wordt verzonden met een link om te bekijken en te ondertekenen.
|
||||||
|
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: E-mail met alleen weergave die wordt verzonden naar ontvangers die de documenten alleen hoeven te bekijken.
|
||||||
|
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Herinnert ontvangers die hun deel nog niet hebben voltooid eraan te ondertekenen, volgens het herinneringsschema.
|
||||||
|
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: Sms met het handtekeningverzoek die naar de ontvangers wordt verzonden met een link om te bekijken en te ondertekenen.
|
||||||
|
phone_2fa_verification_text_message: Sms-bericht voor 2FA-verificatie via telefoon.
|
||||||
|
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Meldingse-mail die naar de verzendende gebruiker en de BCC-adressen wordt gestuurd zodra de indiening door alle partijen is voltooid.
|
||||||
|
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: E-mail met een kopie van de voltooide documenten die na voltooiing naar de ontvangers wordt verzonden.
|
||||||
form_custom_css: Aangepaste CSS voor formulier
|
form_custom_css: Aangepaste CSS voor formulier
|
||||||
subject: Onderwerp
|
subject: Onderwerp
|
||||||
body: Inhoud
|
body: Inhoud
|
||||||
@@ -8239,6 +8294,7 @@ nl: &nl
|
|||||||
invite: Uitnodiging
|
invite: Uitnodiging
|
||||||
link: Link
|
link: Link
|
||||||
mcp: MCP
|
mcp: MCP
|
||||||
|
self: Zelf ondertekenen
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: "<b>E-mail verzonden</b> naar %{submitter_name}"
|
send_email_to_html: "<b>E-mail verzonden</b> naar %{submitter_name}"
|
||||||
bounce_email_html: "<b>E-mail geretourneerd</b> %{submitter_name}"
|
bounce_email_html: "<b>E-mail geretourneerd</b> %{submitter_name}"
|
||||||
|
|||||||
+2
-1
@@ -142,7 +142,8 @@ Rails.application.routes.draw do
|
|||||||
get :completed
|
get :completed
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :resubmit_form, controller: 'start_form', only: :update
|
resource :resubmit_form, controller: 'start_form_resubmit', only: :update
|
||||||
|
resources :start_form_self, only: :update
|
||||||
resource :submit_form_email_2fa, only: %i[create update]
|
resource :submit_form_email_2fa, only: %i[create update]
|
||||||
resources :start_form_email_2fa_send, only: :create
|
resources :start_form_email_2fa_send, only: :create
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
development:
|
|
||||||
secret_key_base: 5e626815c7ef00cc5d7b46ff3099160f25e5271b00ced3b8eba1d5045eb0db0ddb925c85b2fea6b2acb4cbdcb779ba9c5a31600f31e6ee8944ada47a768f6a7f
|
|
||||||
|
|
||||||
test:
|
|
||||||
secret_key_base: 7d754998067691dc61859c6c3109966c44e1756a46fd2409f1d81403599605c103ff84b12102673b498bdbaba04d9949d8d7a2f7af47529ee6dc4dd19f0aef3e
|
|
||||||
|
|
||||||
production:
|
|
||||||
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddSubmissionsAccountTemplateStatistics < ActiveRecord::Migration[8.1]
|
||||||
|
def up
|
||||||
|
return unless adapter_name == 'PostgreSQL'
|
||||||
|
|
||||||
|
execute <<~SQL.squish
|
||||||
|
CREATE STATISTICS IF NOT EXISTS submissions_account_template_stats (dependencies, ndistinct)
|
||||||
|
ON account_id, template_id FROM submissions
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
return unless adapter_name == 'PostgreSQL'
|
||||||
|
|
||||||
|
execute 'DROP STATISTICS IF EXISTS submissions_account_template_stats'
|
||||||
|
end
|
||||||
|
end
|
||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2026_07_07_055354) do
|
ActiveRecord::Schema[8.1].define(version: 2026_08_19_091500) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ module Submissions
|
|||||||
|
|
||||||
[
|
[
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
[{ text: document.filename.to_s }]
|
[{ text: TextUtils.maybe_rtl_reverse(document.filename.to_s) }]
|
||||||
),
|
),
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
[
|
[
|
||||||
@@ -272,7 +272,9 @@ module Submissions
|
|||||||
[
|
[
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
[
|
[
|
||||||
submission.template_submitters.size > 1 && { text: "#{item['name']}\n" },
|
submission.template_submitters.size > 1 && {
|
||||||
|
text: "#{TextUtils.maybe_rtl_reverse(item['name'].to_s)}\n"
|
||||||
|
},
|
||||||
submitter.email && { text: "#{submitter.email}\n", font: [FONT_NAME, { variant: :bold }] },
|
submitter.email && { text: "#{submitter.email}\n", font: [FONT_NAME, { variant: :bold }] },
|
||||||
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
|
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
|
||||||
submitter.phone && { text: "#{submitter.phone}\n" }
|
submitter.phone && { text: "#{submitter.phone}\n" }
|
||||||
@@ -425,7 +427,7 @@ module Submissions
|
|||||||
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
{ link:, text: "#{attachment.filename}\n", style: :link }
|
{ link:, text: "#{TextUtils.maybe_rtl_reverse(attachment.filename.to_s)}\n", style: :link }
|
||||||
end,
|
end,
|
||||||
padding: [0, 0, 10, 0]
|
padding: [0, 0, 10, 0]
|
||||||
)
|
)
|
||||||
@@ -476,6 +478,8 @@ module Submissions
|
|||||||
submitter.name || submitter.email || submitter.phone
|
submitter.name || submitter.email || submitter.phone
|
||||||
end
|
end
|
||||||
|
|
||||||
|
submitter_name = TextUtils.maybe_rtl_reverse(submitter_name.to_s)
|
||||||
|
|
||||||
text =
|
text =
|
||||||
if event.event_type == 'complete_verification'
|
if event.event_type == 'complete_verification'
|
||||||
I18n.t('submission_event_names.complete_verification_by_html', provider: event.data['method'],
|
I18n.t('submission_event_names.complete_verification_by_html', provider: event.data['method'],
|
||||||
@@ -483,18 +487,22 @@ module Submissions
|
|||||||
elsif event.event_type == 'invite_party' &&
|
elsif event.event_type == 'invite_party' &&
|
||||||
(invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
|
(invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
|
||||||
(name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name'))
|
(name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name'))
|
||||||
invited_submitter_name = [invited_submitter.name || invited_submitter.email || invited_submitter.phone,
|
invited_submitter_name = TextUtils.maybe_rtl_reverse(
|
||||||
name].join(' ')
|
[invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' ')
|
||||||
|
)
|
||||||
I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:,
|
I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:,
|
||||||
submitter_name:)
|
submitter_name:)
|
||||||
elsif with_audit_sender && (event.event_type == 'send_email' || event.event_type == 'send_sms')
|
elsif with_audit_sender && (event.event_type == 'send_email' || event.event_type == 'send_sms')
|
||||||
|
created_by_name = TextUtils.maybe_rtl_reverse(submission.created_by_user.full_name)
|
||||||
|
|
||||||
[
|
[
|
||||||
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:),
|
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:),
|
||||||
"<b>#{I18n.t(:from)}</b> #{submission.created_by_user.full_name} #{submission.created_by_user.email}"
|
"<b>#{I18n.t(:from)}</b> #{created_by_name} #{submission.created_by_user.email}"
|
||||||
].join("\n")
|
].join("\n")
|
||||||
elsif event.event_type == 'delegate_form'
|
elsif event.event_type == 'delegate_form'
|
||||||
from = event.data['old_email'].presence ||
|
from = event.data['old_email'].presence ||
|
||||||
versions.rfind { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone }
|
versions.rfind { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone }
|
||||||
|
from = TextUtils.maybe_rtl_reverse(from.to_s)
|
||||||
I18n.t('submission_event_names.delegate_form_by_html', from:, to: event.data['email'])
|
I18n.t('submission_event_names.delegate_form_by_html', from:, to: event.data['email'])
|
||||||
elsif event.event_type.include?('send_')
|
elsif event.event_type.include?('send_')
|
||||||
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:)
|
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:)
|
||||||
|
|||||||
@@ -346,8 +346,11 @@ module Submissions
|
|||||||
(with_signature_id_completed_at ? submitter.completed_at : nil) || attachment.created_at
|
(with_signature_id_completed_at ? submitter.completed_at : nil) || attachment.created_at
|
||||||
|
|
||||||
if with_signature_id_reason || field.dig('preferences', 'reasons').present?
|
if with_signature_id_reason || field.dig('preferences', 'reasons').present?
|
||||||
"#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \
|
reason_line =
|
||||||
"#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}\n" \
|
"#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \
|
||||||
|
"#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}"
|
||||||
|
|
||||||
|
"#{TextUtils.maybe_rtl_reverse(reason_line)}\n" \
|
||||||
"#{I18n.l(signature_timestamp.in_time_zone(timezone), format: time_format)} " \
|
"#{I18n.l(signature_timestamp.in_time_zone(timezone), format: time_format)} " \
|
||||||
"#{TimeUtils.timezone_abbr(timezone, signature_timestamp)}"
|
"#{TimeUtils.timezone_abbr(timezone, signature_timestamp)}"
|
||||||
else
|
else
|
||||||
@@ -509,7 +512,9 @@ module Submissions
|
|||||||
cv.image(PdfIcons.paperclip_io, at: [0, 0], width: box.content_width)
|
cv.image(PdfIcons.paperclip_io, at: [0, 0], width: box.content_width)
|
||||||
end
|
end
|
||||||
|
|
||||||
acc << HexaPDF::Layout::TextFragment.create("#{attachment.filename}\n", font:, font_size:)
|
acc << HexaPDF::Layout::TextFragment.create(
|
||||||
|
"#{TextUtils.maybe_rtl_reverse(attachment.filename.to_s)}\n", font:, font_size:
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
lines = layouter.fit(items, area['w'] * width, height).lines
|
lines = layouter.fit(items, area['w'] * width, height).lines
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module StartForm
|
||||||
|
COOKIES_TTL = 12.hours
|
||||||
|
START_FORM_COOKIES_TTL = 7.days
|
||||||
|
RESUBMIT_TTL = 3.days
|
||||||
|
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||||
|
|
||||||
|
NotSaved = Class.new(StandardError)
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def can_resubmit?(submitter)
|
||||||
|
submitter.completed_at? && submitter.completed_at > RESUBMIT_TTL.ago &&
|
||||||
|
%w[api embed mcp].exclude?(submitter.submission.source) &&
|
||||||
|
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_reopen?(template, submitter, request:, current_user:)
|
||||||
|
return false if submitter.submission&.completed_at? && submitter.viewer?
|
||||||
|
return true if request.cookie_jar.encrypted[:start_form_slug] == submitter.slug
|
||||||
|
return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug
|
||||||
|
return false if submitter.email.blank?
|
||||||
|
return true if submitter.completed_at?
|
||||||
|
return false if submitter.submission.source_embed?
|
||||||
|
return true if template.preferences['shared_link_2fa'] == true
|
||||||
|
return false if submitter.submission.source_link?
|
||||||
|
return true if current_user && submitter.email == current_user.email &&
|
||||||
|
current_user.account_id == submitter.account_id
|
||||||
|
|
||||||
|
Docuseal.multitenant? || Accounts.can_send_emails?(template.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_start_form_cookie(submitter, request)
|
||||||
|
request.cookie_jar.encrypted[:start_form_slug] =
|
||||||
|
{ value: submitter.slug, expires: START_FORM_COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_initialize_submitter(template, submitter_params, exclude_completed:, request:, current_user: nil)
|
||||||
|
required_fields = template.preferences.fetch('link_form_fields', ['email'])
|
||||||
|
|
||||||
|
required_params = required_fields.index_with { |key| submitter_params[key] }
|
||||||
|
|
||||||
|
find_params = required_params.except('name')
|
||||||
|
|
||||||
|
submitter =
|
||||||
|
if find_params.compact_blank.blank?
|
||||||
|
Submitter.new
|
||||||
|
else
|
||||||
|
build_submitters_scope(template, exclude_completed:).find_or_initialize_by(find_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
if submitter.persisted? && !can_reopen?(template, submitter, request:, current_user:)
|
||||||
|
submitter = Submitter.new(find_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.name = required_params['name'] if submitter.new_record?
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_submitters_scope(template, exclude_completed: false, source: nil)
|
||||||
|
submissions = template.submissions.non_expired.active
|
||||||
|
submissions = submissions.where(source:) if source
|
||||||
|
|
||||||
|
Submitter
|
||||||
|
.where(submission: submissions)
|
||||||
|
.order(id: :desc)
|
||||||
|
.where(declined_at: nil)
|
||||||
|
.where(external_id: nil)
|
||||||
|
.then { |rel| exclude_completed ? rel.where(completed_at: nil) : rel }
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_submission_attributes(submitter, template, ip:, user_agent:, source: :link, resubmit_submitter: nil)
|
||||||
|
submitter.assign_attributes(
|
||||||
|
uuid: first_submitter_uuid(template),
|
||||||
|
ip:,
|
||||||
|
ua: user_agent,
|
||||||
|
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||||
|
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
||||||
|
metadata: resubmit_submitter&.metadata.presence || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
submitter.assign_attributes(resubmit_submitter.slice(:name, :email, :phone)) if resubmit_submitter
|
||||||
|
|
||||||
|
if submitter.values.present?
|
||||||
|
resubmit_submitter.attachments.each do |attachment|
|
||||||
|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.submission ||= Submission.new(template:,
|
||||||
|
account_id: template.account_id,
|
||||||
|
template_submitters: template.submitters,
|
||||||
|
expire_at: Templates.build_default_expire_at(template),
|
||||||
|
submitters: [submitter],
|
||||||
|
source:)
|
||||||
|
|
||||||
|
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission)
|
||||||
|
|
||||||
|
submitter.account_id = submitter.submission.account_id
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_submitter_uuid(template)
|
||||||
|
(Templates.filter_undefined_submitters(template.submitters).first || template.submitters.first)['uuid']
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_2fa_and_save_submitter(submitter, request, is_new_record:)
|
||||||
|
is_otp_verified = Submitters.verify_link_otp!(request.params[:one_time_code], submitter)
|
||||||
|
|
||||||
|
return false if !is_otp_verified && request.cookie_jar.encrypted[:email_2fa_slug] != submitter.slug
|
||||||
|
|
||||||
|
raise NotSaved unless submitter.save
|
||||||
|
|
||||||
|
enqueue_new_submitter_jobs(submitter) if is_new_record
|
||||||
|
|
||||||
|
assign_start_form_cookie(submitter, request)
|
||||||
|
|
||||||
|
if is_otp_verified
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
|
||||||
|
|
||||||
|
request.cookie_jar.encrypted[:email_2fa_slug] =
|
||||||
|
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_new_submitter_jobs(submitter)
|
||||||
|
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(submitter)
|
||||||
|
|
||||||
|
expire_at = submitter.submission.expire_at
|
||||||
|
|
||||||
|
return unless expire_at
|
||||||
|
|
||||||
|
ProcessSubmissionExpiredJob.perform_at(expire_at, 'submission_id' => submitter.submission_id,
|
||||||
|
'expire_at' => expire_at.to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -41,16 +41,24 @@ module Templates
|
|||||||
attachment
|
attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
def process(attachment, data, extract_fields: false)
|
def process(attachment, data, extract_fields: false, doc: nil)
|
||||||
if attachment.content_type == PDF_CONTENT_TYPE && extract_fields && data.size < MAX_FLATTEN_FILE_SIZE
|
if attachment.content_type == PDF_CONTENT_TYPE && extract_fields && data.size < MAX_FLATTEN_FILE_SIZE
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
if doc
|
||||||
|
fields = Templates::FindPdfiumAcroFields.call(attachment, doc, data)
|
||||||
|
else
|
||||||
|
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
pdf ||= HexaPDF::Document.new(io: StringIO.new(data))
|
if doc
|
||||||
|
number_of_pages = doc.page_count
|
||||||
|
else
|
||||||
|
pdf ||= HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
number_of_pages = pdf.pages.size
|
number_of_pages = pdf.pages.size
|
||||||
|
end
|
||||||
|
|
||||||
attachment.metadata['pdf'] ||= {}
|
attachment.metadata['pdf'] ||= {}
|
||||||
attachment.metadata['pdf']['number_of_pages'] = number_of_pages
|
attachment.metadata['pdf']['number_of_pages'] = number_of_pages
|
||||||
|
|||||||
Reference in new issue
Block a user