view only party

pull/698/merge
Pete Matsyburka 1 month ago
parent b6fb7c6977
commit e88874e757

@ -30,7 +30,7 @@ module Api
private
def build_completed_documents(submission, merge: false)
last_submitter = submission.submitters.max_by(&:completed_at)
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
if merge
if submission.merged_document_attachment.blank?

@ -80,8 +80,9 @@ module Api
Submissions.send_signature_requests(submissions)
submissions.each do |submission|
if submission.submitters.all?(&:completed_at?) && Submissions.maybe_update_completed_at(submission)
last_submitter = submission.submitters.max_by(&:completed_at)
if submission.submitters.all? { |s| s.viewer? || s.completed_at? } &&
Submissions.maybe_update_completed_at(submission)
last_submitter = submission.submitters.reject(&:viewer?).max_by(&:completed_at)
end
submission.submitters.each do |submitter|

@ -87,6 +87,8 @@ class SubmissionsController < ApplicationController
private
def create_submissions(template, submissions_params, params)
normalize_message_submitter_uuids!(params)
submissions_attrs = submissions_params[:submission].to_h.values
submissions_attrs, _, new_fields =
@ -111,4 +113,23 @@ class SubmissionsController < ApplicationController
def submissions_params
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
end
def normalize_message_submitter_uuids!(params)
return if params[:request_email_per_submitter] == '1'
uuids = params[:email_message_submitter_uuids]
return if uuids.blank?
return if params[:subject].blank? && params[:body].blank?
params[:submitter_preferences] =
Array.wrap(uuids).index_with { { 'subject' => params[:subject], 'body' => params[:body] } }
params[:request_email_per_submitter] = '1'
params.delete(:subject)
params.delete(:body)
params
end
end

@ -71,6 +71,12 @@ class SubmitFormController < ApplicationController
status: :unprocessable_content
end
if @submitter.viewer?
Rollbar.warning("Submit viewer: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: I18n.t('form_is_view_only') }, status: :unprocessable_content
end
Submitters::SubmitValues.call(@submitter, params, request)
head :ok

@ -13,6 +13,7 @@ class SubmitFormDeclineController < ApplicationController
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
@submitter.viewer? ||
!Submitters::AuthorizedForForm.call(@submitter,
current_user,
request)

@ -12,6 +12,7 @@ class SubmitFormDelegateController < ApplicationController
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
@submitter.viewer? ||
!Submitters::AuthorizedForForm.call(@submitter,
current_user,
request)

@ -12,6 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return redirect_to submit_form_path(@submitter.slug) if @submitter.viewer?
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
return redirect_to submit_form_path(@submitter.slug)

@ -48,6 +48,7 @@ class SubmitFormInviteController < ApplicationController
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at? &&
!submitter.viewer? &&
Submitters::AuthorizedForForm.call(submitter, current_user, request)
end

@ -58,7 +58,7 @@ class SubmittersController < ApplicationController
if params[:send_email] == '1' && submitter.email.present?
is_sent_recently = Docuseal.multitenant? &&
EmailEvent.exists?(email: submitter.email,
tag: 'submitter_invitation',
tag: %w[submitter_invitation submitter_view_invitation],
emailable: submitter,
event_type: 'send',
created_at: 4.hours.ago..Time.current)

@ -5,6 +5,8 @@ class TemplatesPreferencesController < ApplicationController
RESETTABLE_PREFERENCE_KEYS = {
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY => %w[invitation_view_email_subject
invitation_view_email_body],
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
invitation_reminder_email_body],
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
@ -48,12 +50,12 @@ class TemplatesPreferencesController < ApplicationController
def template_params
params.require(:template).permit(
preferences: %i[bcc_completed request_email_subject request_email_body
invitation_view_email_subject invitation_view_email_body
invitation_reminder_email_subject invitation_reminder_email_body
documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters
completed_notification_email_attach_documents completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa require_email_2fa
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
completed_notification_email_subject completed_notification_email_body

@ -147,7 +147,7 @@
@submit.prevent="submitStep"
/>
<button
v-if="!isFormVisible"
v-if="!isFormVisible && currentField"
id="expand_form_button"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
style="width: 96%; margin-left: 2%"
@ -174,6 +174,7 @@
/>
</button>
<div
v-if="currentField"
v-show="isFormVisible"
id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
@ -1172,7 +1173,11 @@ export default {
}
},
isAnonymousChecboxes () {
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
if (this.currentField) {
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
} else {
return false
}
},
isButtonDisabled () {
if (this.recalculateButtonDisabledKey) {

@ -3206,15 +3206,6 @@ export default {
e.preventDefault()
alert(this.t('please_draw_fields_to_prepare_the_document'))
} else {
const submitterWithoutFields =
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
if (submitterWithoutFields) {
e.preventDefault()
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
}
}
},
onSaveClick () {
@ -3231,32 +3222,25 @@ export default {
if (!this.template.fields.length) {
alert(this.t('please_draw_fields_to_prepare_the_document'))
} else {
const submitterWithoutFields =
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
if (submitterWithoutFields) {
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
} else {
this.isSaving = true
this.isSaving = true
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
dynamicDocumentRefs.map((ref) => ref.update())
dynamicDocumentRefs.map((ref) => ref.update())
this.rebuildVariablesSchema({ disable: false })
this.rebuildVariablesSchema({ disable: false })
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => {
if (this.withRevisions) {
this.captureRevision()
}
Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => {
if (this.withRevisions) {
this.captureRevision()
}
window.Turbo.visit(`/templates/${this.template.id}`)
}).finally(() => {
this.isSaving = false
})
}
window.Turbo.visit(`/templates/${this.template.id}`)
}).finally(() => {
this.isSaving = false
})
}
},
scrollToArea (area) {

@ -13,7 +13,12 @@ class ProcessSubmitterCompletionJob
if params.key?('is_last')
params['is_last']
else
!submission.submitters.exists?(completed_at: nil) &&
viewer_uuids = submission.template_submitters.to_a.filter_map { |s| s['uuid'] if s['is_viewer'] }
incomplete = submission.submitters.where(completed_at: nil)
incomplete = incomplete.where.not(uuid: viewer_uuids) if viewer_uuids.present?
!incomplete.exists? &&
submitter.completed_at == submission.submitters.maximum(:completed_at)
end
@ -33,7 +38,9 @@ class ProcessSubmitterCompletionJob
if !submission.completed_at && submission.submitters_order_preserved? && params['send_invitation_email'] != false &&
Submission.exists?(id: submission.id, completed_at: nil)
enqueue_next_submitter_request_notification(submitter)
next_submitters = enqueue_next_submitter_request_notification(submitter)
enqueue_next_submitter_viewer_notification(submission, next_submitters) unless is_last
end
enqueue_completed_webhooks(submitter, is_last:)
@ -145,7 +152,7 @@ class ProcessSubmitterCompletionJob
return if configs.value['enabled'] == false
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name)
.sort_by { |e| e.completed_at || Time.current }.select(&:email?).map(&:friendly_name)
return if to.blank?
@ -165,9 +172,9 @@ class ProcessSubmitterCompletionJob
bcc.to_s.scan(User::EMAIL_REGEXP)
end
def enqueue_next_submitter_request_notification(submitter)
def enqueue_next_submitter_request_notification(submitter) # rubocop:disable Metrics/PerceivedComplexity
submission = submitter.submission
submitters_index = submission.submitters.index_by(&:uuid)
submitters_index = submission.submitters.reject(&:viewer?).index_by(&:uuid)
next_submitter_items =
if submission.template_submitters.any? { |s| s['order'] }
@ -196,5 +203,43 @@ class ProcessSubmitterCompletionJob
next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact
Submitters.send_signature_requests(next_submitters)
next_submitters
end
def enqueue_next_submitter_viewer_notification(submission, next_submitters)
viewers = submission.submitters.select(&:viewer?)
return [] if viewers.blank?
next_submitter_uuids = next_submitters.to_set(&:uuid)
viewers_index = viewers.index_by(&:uuid)
next_viewers =
if submission.template_submitters.any? { |s| s['order'] }
next_orders = submission.template_submitters
.select { |s| next_submitter_uuids.include?(s['uuid']) }
.pluck('order')
submission.template_submitters.filter_map do |s|
viewers_index[s['uuid']] if next_orders.include?(s['order'])
end
else
preceding_submitter_uuid = nil
submission.template_submitters.filter_map do |template_submitter|
viewer = viewers_index[template_submitter['uuid']]
if viewer
viewer if next_submitter_uuids.include?(preceding_submitter_uuid)
else
preceding_submitter_uuid = template_submitter['uuid']
nil
end
end
end
Submitters.send_signature_requests(next_viewers)
end
end

@ -19,7 +19,12 @@ class SendSubmitterInvitationEmailJob
return
end
mail = SubmitterMailer.invitation_email(submitter)
mail =
if submitter.viewer?
SubmitterMailer.invitation_view_email(submitter)
else
SubmitterMailer.invitation_email(submitter)
end
Submitters::ValidateSending.call(submitter, mail)

@ -45,6 +45,45 @@ class SubmitterMailer < ApplicationMailer
end
end
def invitation_view_email(submitter)
@current_account = submitter.submission.account
@submitter = submitter
if submitter.preferences['email_message_uuid']
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end
template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
@body = @email_message&.normalized_body.presence ||
@submitter.template&.preferences&.dig('invitation_view_email_body').presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence
@subject = @email_message&.subject.presence ||
@submitter.template&.preferences&.dig('invitation_view_email_subject').presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY)
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_view_invitation', @submitter)
reply_to = build_submitter_reply_to(@submitter, email_config: @email_config)
maybe_set_custom_domain(@submitter)
I18n.with_locale(@current_account.locale) do
subject = build_invite_subject(@subject, @email_config, submitter)
mail(
to: @submitter.friendly_name,
from: from_address_for_submitter(submitter),
subject:,
reply_to:
)
end
end
def completed_email(submitter, user, to: nil)
@current_account = submitter.submission.account
@submitter = submitter
@ -216,6 +255,8 @@ class SubmitterMailer < ApplicationMailer
def build_invite_subject(subject, email_config, submitter)
if email_config || subject
ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:)
elsif submitter.viewer?
I18n.t(:you_are_invited_to_view_a_document)
elsif submitter.with_signature_fields?
I18n.t(:you_are_invited_to_sign_a_document)
else

@ -22,6 +22,7 @@
#
class AccountConfig < ApplicationRecord
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
SUBMITTER_VIEW_INVITATION_EMAIL_KEY = 'submitter_view_invitation_email'
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
@ -63,6 +64,7 @@ class AccountConfig < ApplicationRecord
EMAIL_VARIABLES = {
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_VIEW_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_COMPLETED_EMAIL_KEY => %w[template.name submission.submitters submission.link].freeze,
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[template.name documents.link account.name].freeze
@ -75,6 +77,12 @@ class AccountConfig < ApplicationRecord
'body' => I18n.t(:submitter_invitation_email_sign_body)
}
},
SUBMITTER_VIEW_INVITATION_EMAIL_KEY => lambda {
{
'subject' => I18n.t(:you_are_invited_to_view_a_document),
'body' => I18n.t(:submitter_invitation_email_view_body)
}
},
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda {
{
'subject' => I18n.t(:you_are_invited_to_sign_a_document),

@ -116,6 +116,12 @@ class Submitter < ApplicationRecord
end
end
def viewer?
return false if submission.template_submitters.blank?
submission.template_submitters.any? { |s| s['uuid'] == uuid && s['is_viewer'] }
end
private
def anonymize_email_events

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2" />
<path d="M9 15l2 2l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2" />
<path d="M12 17v.01" />
<path d="M12 14a1.5 1.5 0 1 0 -1.14 -2.474" />
</svg>

After

Width:  |  Height:  |  Size: 482 B

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M16 7h4" />
<path d="M16 16v1l2 2l.5 -.5m1.5 -2.5v-11c0 -1.121 -.879 -2 -2 -2s-2 .879 -2 2v7" />
<path d="M18 19h-13a2 2 0 1 1 0 -4h4a2 2 0 1 0 0 -4h-3" />
<path d="M3 3l18 18" />
</svg>

After

Width:  |  Height:  |  Size: 480 B

@ -92,7 +92,7 @@
<%= local_assigns[:variables_form] %>
<div>
<%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails: %>
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
<% if has_phone_field %>
<%= render 'send_sms', f: %>
<% end %>

@ -70,7 +70,7 @@
<%= local_assigns[:variables_form] %>
<div>
<%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails: %>
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
<%= render 'extra_fields', f: %>
</div>
<div class="form-control">

@ -38,9 +38,20 @@
<% end %>
</div>
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<% view_config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY) %>
<% view_template_subject = template&.preferences&.dig('invitation_view_email_subject').presence %>
<% view_template_body = template&.preferences&.dig('invitation_view_email_body').presence %>
<% default_subject = template&.preferences&.dig('request_email_subject').presence || config.value['subject'] %>
<% default_body = template&.preferences&.dig('request_email_body').presence || config.value['body'] %>
<% is_edit_viewer = local_assigns[:submitter] && local_assigns[:viewer_submitter_uuids].include?(local_assigns[:submitter].uuid) %>
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
<div class="card-body">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template_submitters.size < 5 && template&.preferences&.dig('submitters').to_a.size > 1 %>
<% if local_assigns[:viewer_submitter_uuids].present? && local_assigns[:submitter].blank? %>
<% (template_submitters.pluck('uuid') - local_assigns[:viewer_submitter_uuids].to_a).each do |signer_uuid| %>
<%= hidden_field_tag 'email_message_submitter_uuids[]', signer_uuid %>
<% end %>
<% end %>
<div class="peer-checked:hidden form-control space-y-2">
<div class="form-control">
<div class="flex justify-between">
@ -51,12 +62,12 @@
</label>
<% end %>
</div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || (is_edit_viewer ? view_template_subject : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || (is_edit_viewer ? view_config.value['subject'] : default_subject), required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<%= f.label :message, t('body'), class: 'label' %>
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
<%= render 'personalization_settings/markdown_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
<%= render 'personalization_settings/markdown_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_edit_viewer ? view_template_body : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || (is_edit_viewer ? view_config.value['body'] : default_body), variables: body_variables %>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
@ -81,17 +92,18 @@
</ul>
</toggle-visible>
<% template_submitters.each_with_index do |submitter, index| %>
<% is_viewer = local_assigns[:viewer_submitter_uuids].include?(submitter['uuid']) %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %>
</div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || (is_viewer ? view_template_subject : nil) || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || (is_viewer ? view_config.value['subject'] : default_subject), required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<%= ff.label :message, t('body'), class: 'label' %>
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_viewer ? view_template_body : nil) || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || (is_viewer ? view_config.value['body'] : default_body), variables: body_variables %>
</div>
</div>
<% end %>

@ -1,6 +1,7 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
<% viewer_submitter_uuids = Set.new(@template.submitters.pluck('uuid')) - Set.new(@template.fields.to_a.pluck('submitter_uuid')) %>
<% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %>
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
@ -26,18 +27,18 @@
<div class="px-5 mb-5 mt-4">
<% unless only_detailed %>
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
<%= render 'email_form', template: @template, variables_form:, can_send_emails: %>
<%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
</div>
<div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
<%= render 'phone_form', template: @template, variables_form: %>
</div>
<% end %>
<div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>">
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields:, variables_form:, can_send_emails: %>
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields:, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
</div>
<% if with_list %>
<div id="list" class="hidden">
<%= render 'list_form', template: @template, can_send_emails: %>
<%= render 'list_form', template: @template, can_send_emails:, viewer_submitter_uuids: %>
</div>
<% end %>
<%= render 'submissions/error' %>

@ -178,7 +178,7 @@
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span>
</div>
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !@submission.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %>
@ -211,7 +211,11 @@
</div>
<% end %>
<div class="flex items-center space-x-1 mt-1">
<% if @submission.expire_at? && submitter && !submitter.completed_at? %>
<% if submitter&.viewer? %>
<%= svg_icon(submitter.opened_at? ? 'file_check' : 'file_unknown', class: 'w-5 h-5') %>
<% elsif submitter && !submitter.completed_at? && @submission.completed_at? %>
<%= svg_icon('writing_off', class: 'w-5 h-5') %>
<% elsif @submission.expire_at? && submitter && !submitter.completed_at? %>
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %>
<% else %>
<%= svg_icon('writing', class: 'w-5 h-5') %>
@ -219,9 +223,19 @@
<span>
<% if submitter&.declined_at? %>
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
<% elsif submitter&.viewer? %>
<% if submitter.opened_at? %>
<%= t('viewed_on_time', time: l(submitter.opened_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
<% elsif @submission.completed_at? %>
<%= t('not_viewed') %>
<% else %>
<%= t('not_viewed_yet') %>
<% end %>
<% elsif submitter %>
<% if submitter.completed_at? %>
<%= l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) %>
<% elsif @submission.completed_at? %>
<%= t('not_completed') %>
<% elsif @submission.expire_at? %>
<% if @submission.expired? %>
<%= t(:expired) %>
@ -244,15 +258,15 @@
</span>
</div>
<% end %>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && (Docuseal.multitenant? || Accounts.can_send_emails?(current_account)) && !@submission.expired? && !submitter.declined_at? %>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && (Docuseal.multitenant? || Accounts.can_send_emails?(current_account)) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1">
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter), class: 'btn btn-sm btn-primary w-full' %>
</div>
<% end %>
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
<%= render 'submissions/send_sms_button', submitter: %>
<% end %>
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
<% if signed_in? && submitter && !submitter.viewer? && !submitter.completed_at? && !@submission.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1">
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
<%= t('sign_in_person') %>

@ -23,7 +23,7 @@
<%= @submitter.submission.name || @submitter.submission.template&.name %>
</h1>
<div class="flex items-center gap-2 group" style="margin-left: 20px; flex-shrink: 0">
<% if @form_configs[:with_decline] %>
<% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id = SecureRandom.uuid %>" class="hidden group-has-[.complete-button]:flex">
<button type="button" class="btn btn-sm md:!px-5 px-2" aria-label="<%= t(:decline) %>">
<span class="hidden md:inline"><%= t(:decline) %></span>
@ -34,11 +34,11 @@
</modal-button>
<% end %>
<span id="complete_button_container" class="peer contents"></span>
<% if @form_configs[:with_delegate] %>
<% if @form_configs[:with_delegate] && !@submitter.viewer? %>
<modal-button data-target="<%= delegate_modal_id = SecureRandom.uuid %>" class="hidden peer-empty:flex">
<button id="delegate_button" type="button" class="btn btn-sm !px-5"><%= t(:delegate) %></button>
</modal-button>
<% if @form_configs[:with_decline] %>
<% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
<%= svg_icon('x', class: 'w-5 h-5') %>
@ -56,7 +56,7 @@
</download-button>
<% end %>
<% else %>
<% if @form_configs[:with_decline] %>
<% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button" type="button" class="btn btn-sm !px-5"><%= t(:decline) %></button>
</modal-button>
@ -77,7 +77,7 @@
</div>
</header>
<scroll-buttons inert class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10 group">
<% if @form_configs[:with_decline] %>
<% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden group-has-[.complete-button]:flex">
<button type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
<%= svg_icon('x', class: 'w-5 h-5') %>
@ -85,7 +85,7 @@
</modal-button>
<% end %>
<span id="complete_button_container_scroll" class="peer contents"></span>
<% if @form_configs[:with_delegate] %>
<% if @form_configs[:with_delegate] && !@submitter.viewer? %>
<modal-button data-target="<%= delegate_modal_id %>" class="hidden peer-empty:flex">
<button id="delegate_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:delegate) %>">
<span class="min-[1366px]:inline hidden px-3">
@ -96,7 +96,7 @@
</span>
</button>
</modal-button>
<% if @form_configs[:with_decline] %>
<% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button_mobile" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
<%= svg_icon('x', class: 'w-5 h-5') %>
@ -104,7 +104,7 @@
</modal-button>
<% end %>
<% else %>
<% if @form_configs[:with_decline] %>
<% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:decline) %>">
<span class="min-[1366px]:inline hidden px-3">
@ -177,12 +177,12 @@
</div>
</div>
</div>
<% if @form_configs[:with_decline] %>
<% if @form_configs[:with_decline] && !@submitter.viewer? %>
<%= render 'shared/html_modal', title: t(:decline), uuid: decline_modal_id do %>
<%= render 'submit_form/decline_form', submitter: @submitter %>
<% end %>
<% end %>
<% if @form_configs[:with_delegate] %>
<% if @form_configs[:with_delegate] && !@submitter.viewer? %>
<%= render 'shared/html_modal', title: t(:delegate), uuid: delegate_modal_id do %>
<%= render 'submit_form/delegate_form', submitter: @submitter %>
<% end %>

@ -0,0 +1,14 @@
<% if @body.present? %>
<%= render 'custom_content', content: @body, submitter: @submitter %>
<% if !@body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !@body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !@body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !@body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<% end %>
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= I18n.t(:you_have_been_invited_to_view_the_name, name: @submitter.submission.name || @submitter.submission.template&.name) %></p>
<p><%= link_to I18n.t(:view_document), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>
</p>
<% end %>

@ -17,7 +17,8 @@
</submitter-item>
</div>
<div>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false, can_send_emails: Accounts.can_send_emails?(current_account) %>
<% viewer_submitter_uuids = Set.new((@submitter.submission.template_submitters || @submitter.template.submitters).pluck('uuid')) - Set.new((@submitter.submission.template_fields || @submitter.template.fields).to_a.pluck('submitter_uuid')) %>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false, can_send_emails: Accounts.can_send_emails?(current_account), viewer_submitter_uuids: %>
<%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %>
</div>
<div class="form-control mt-4">

@ -1,5 +1,6 @@
<% close_on_submit = local_assigns.fetch(:close_on_submit, true) %>
<% is_order_set = template.submitters.any? { |s| s['order'] } %>
<% field_submitter_uuids = Set.new(template.fields.pluck('submitter_uuid')) %>
<%= form_for template, url: template_recipients_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form }, data: { close_on_submit: } do |f| %>
<% unless close_on_submit %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
@ -10,6 +11,7 @@
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :invite_via_field_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'invite_via_field_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %>
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : (item.invite_via_field_uuid.present? ? 'invite_via_field' : ''))))) %>
<%= ff.hidden_field :uuid %>
<% is_viewer_row = field_submitter_uuids.exclude?(submitter['uuid']) %>
<div class="form-control">
<div class="flex justify-between">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
@ -21,6 +23,8 @@
<mount-on-click data-template-id="order_fields" class="link whitespace-nowrap text-sm mt-1 mr-1 block">
<%= t('edit_order') %>
</mount-on-click>
<% elsif is_viewer_row %>
<span class="inline-flex items-center justify-center h-6 px-2 rounded-full bg-base-200 text-base-content text-xs font-medium normal-case whitespace-nowrap"><%= t('view_only') %></span>
<% end %>
</div>
<% end %>
@ -141,11 +145,13 @@
</div>
<% if template.submitters.size > 2 && !is_order_set %>
<template id="order_fields">
<% last_viewer_order = -1 %>
<% template.submitters.each_with_index do |submitter, index| %>
<% default_order = field_submitter_uuids.include?(submitter['uuid']) ? (last_viewer_order += 1) : [last_viewer_order, 0].max %>
<turbo-stream action="replace" target="order_<%= submitter['uuid'] %>">
<template>
<div id="order_<%= submitter['uuid'] %>">
<%= select_tag "template[submitters][#{index}][order]", options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, submitter['order'].presence || index), class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %>
<%= select_tag "template[submitters][#{index}][order]", options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, default_order), class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %>
</div>
</template>
</turbo-stream>

@ -1,6 +1,9 @@
<div id="<%= AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY %>_form">
<% template_email_preferences_values = @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank %>
<% default_template_email_preferences_values = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body') %>
<% viewer_default_email_preferences_values = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY).value.values_at('subject', 'body') %>
<% viewer_template_email_preferences_values = @template.preferences.values_at('invitation_view_email_subject', 'invitation_view_email_body').compact_blank.presence %>
<% field_submitter_uuids = Set.new(@template.fields.pluck('submitter_uuid')) %>
<% is_custom_template_email = template_email_preferences_values.present? %>
<% multiple_submitters = @template.submitters.size > 1 && @template.submitters.size < 5 %>
<% if is_custom_template_email || @template.preferences['submitters'].to_a.any? %>
@ -49,7 +52,7 @@
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
<% submitter_email_preferences_values = submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence %>
<% submitter_email_values = submitter_email_preferences_values || template_email_preferences_values.presence || default_template_email_preferences_values %>
<% submitter_email_values = field_submitter_uuids.include?(submitter['uuid']) ? (submitter_email_preferences_values || template_email_preferences_values.presence || default_template_email_preferences_values) : (viewer_template_email_preferences_values || submitter_email_preferences_values || viewer_default_email_preferences_values) %>
<%= hidden_field_tag 'template[preferences][submitters][][uuid]', submitter['uuid'] %>
<div class="form-control">
<div class="flex justify-between">

@ -0,0 +1,34 @@
<div id="<%= AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY %>_form">
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY).value %>
<% template_email_preferences_values = @template.preferences.values_at('invitation_view_email_subject', 'invitation_view_email_body').compact_blank.presence %>
<% is_custom_template_email = template_email_preferences_values.present? %>
<% if is_custom_template_email %>
<%= button_to nil, template_preferences_path(@template), id: 'submitter_view_invitation_email_reset_link', method: :delete, class: 'hidden', params: { config_key: AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY }, data: { turbo_confirm: t('are_you_sure_') }, form: { data: { close_on_submit: false } } %>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_view_invitation_email_template_form' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert_view"></toggle-on-submit>
<%= 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="flex justify-between">
<%= ff.label :invitation_view_email_subject, t('email_subject'), class: 'label' %>
<% if is_custom_template_email %>
<label for="submitter_view_invitation_email_reset_link" class="label underline">
<%= t('reset_default') %>
</label>
<% end %>
</div>
<%= ff.text_field :invitation_view_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<%= ff.label :invitation_view_email_body, t('email_body'), class: 'label' %>
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:invitation_view_email_body), value: ff.object.invitation_view_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY] %>
</div>
<% end %>
<% end %>
<div class="form-control pt-2">
<%= button_tag button_title(title: t('save'), disabled_with: t('saving')), form: 'submitter_view_invitation_email_template_form', class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert_view" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
</div>

@ -109,6 +109,18 @@
<%= render 'templates_preferences/submitter_invitation_email_form' %>
</div>
</div>
<% field_submitter_uuids = Set.new(@template.fields.pluck('submitter_uuid')) %>
<% unless @template.submitters.all? { |s| field_submitter_uuids.include?(s['uuid']) } %>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
<%= t('view_documents_email') %>
</div>
<div class="collapse-content">
<%= render 'templates_preferences/submitter_view_invitation_email_form' %>
</div>
</div>
<% end %>
<%= render 'templates_preferences/submitter_invitation_reminder_email_collapse' %>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="checkbox" name="accordion">

@ -92,16 +92,20 @@ en: &en
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature.
you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.'
you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".'
you_have_been_invited_to_view_the_name: 'You have been invited to view the "%{name}".'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: "Alternatively, you can review and download your copy using the link below:"
please_check_the_copy_of_your_name_in_the_email_attachments: 'Please check the copy of your "%{name}" in the email attachments.'
awaiting_completion_by_the_other_party: "Awaiting completion by the other party"
review_and_sign: Review and Sign
view_document: View Document
review_and_submit: Review and Submit
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Please contact us by replying to this email if you have any questions."
submitter_invitation_sms_body_sign: '{account.name} has invited you to sign a document: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} has invited you to view a document: {submitter.link}'
verification_code_sms_body: 'Verification code: {code}'
you_are_invited_to_submit_a_form: 'You are invited to submit a form'
you_are_invited_to_sign_a_document: 'You are invited to sign a document'
you_are_invited_to_view_a_document: 'You are invited to view a document'
you_are_invited_to_sign_documents: 'You are invited to sign documents'
your_document_copy: 'Your document copy'
name_has_been_completed_by_submitters: '"%{name}" has been completed by %{submitters}.'
@ -115,6 +119,17 @@ en: &en
Please contact us by replying to this email if you have any questions.
Thanks,
{account.name}
submitter_invitation_email_view_body: |
Hi there,
You have been invited to view the "{template.name}".
[View Document]({submitter.link})
Please contact us by replying to this email if you have any questions.
Thanks,
{account.name}
submitter_completed_email_body: |
@ -307,6 +322,8 @@ en: &en
invalid_timeserver: Invalid Timeserver
email_templates: Email Templates
signature_request_email: Signature request email
view_documents_email: View documents email
view_only: View only
signature_request_reminder_email: Signature request reminder email
signature_request_sms: Signature Request SMS
verification_code_sms: Verification Code SMS
@ -569,6 +586,7 @@ en: &en
form_has_been_archived: Form has been archived.
form_has_been_expired: Form has been expired.
form_has_been_declined: Form has been declined.
form_is_view_only: Form is view only.
file_is_missing: File is missing
folder_name_has_been_updated: Folder name has been updated.
unable_to_rename_folder: Unable to rename folder.
@ -653,6 +671,10 @@ en: &en
signers: Signers
not_invited_yet: Not invited yet
not_completed_yet: Not completed yet
not_completed: Not completed
not_viewed_yet: Not viewed yet
not_viewed: Not viewed
viewed_on_time: 'Viewed on %{time}'
declined_on_time: 'Declined on %{time}'
expire_on_time: 'Expire on %{time}'
sign_in_person: Sign In-person
@ -1151,15 +1173,19 @@ es: &es
thanks: Gracias
you_have_been_invited_to_submit_the_name_form: 'Has sido invitado/a a enviar el formulario "%{name}".'
you_have_been_invited_to_sign_the_name: 'Has sido invitado/a a firmar el "%{name}".'
you_have_been_invited_to_view_the_name: 'Has sido invitado/a a ver el "%{name}".'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: "Alternativamente, puedes revisar y descargar tu copia usando el enlace a continuación:"
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, revisa la copia de tu "%{name}" en los archivos adjuntos del correo electrónico.'
review_and_sign: Revisar y Firmar
view_document: Ver documento
review_and_submit: Revisar y Enviar
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta."
submitter_invitation_sms_body_sign: '{account.name} te ha invitado a firmar un documento: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} te ha invitado a ver un documento: {submitter.link}'
verification_code_sms_body: 'Código de verificación: {code}'
you_are_invited_to_submit_a_form: 'Estás invitado/a a enviar un formulario'
you_are_invited_to_sign_a_document: 'Estás invitado/a a firmar un documento'
you_are_invited_to_view_a_document: 'Estás invitado/a a ver un documento'
you_are_invited_to_sign_documents: 'Estás invitado/a a firmar documentos'
your_document_copy: 'Tu copia del documento'
name_has_been_completed_by_submitters: '"%{name}" ha sido completado por %{submitters}.'
@ -1173,6 +1199,17 @@ es: &es
Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta.
Gracias,
{account.name}
submitter_invitation_email_view_body: |
Hola,
Has sido invitado/a a ver el "{template.name}".
[Ver documento]({submitter.link})
Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta.
Gracias,
{account.name}
submitter_completed_email_body: |
@ -1364,6 +1401,8 @@ es: &es
invalid_timeserver: Servidor de tiempo inválido
email_templates: Plantillas de correo electrónico
signature_request_email: Correo de solicitud de firma
view_documents_email: Correo de visualización de documentos
view_only: Solo lectura
signature_request_reminder_email: Correo de recordatorio de solicitud de firma
signature_request_sms: SMS de solicitud de firma
verification_code_sms: SMS de código de verificación
@ -1626,6 +1665,7 @@ es: &es
form_has_been_archived: El formulario ha sido archivado.
form_has_been_expired: El formulario ha expirado.
form_has_been_declined: El formulario ha sido rechazado.
form_is_view_only: El formulario es de solo lectura.
file_is_missing: Falta el archivo
folder_name_has_been_updated: El nombre de la carpeta ha sido actualizado.
unable_to_rename_folder: No se pudo renombrar la carpeta.
@ -1710,6 +1750,10 @@ es: &es
signers: Firmantes
not_invited_yet: Aún no invitado
not_completed_yet: Aún no completado
not_completed: No completado
not_viewed_yet: Aún no visto
not_viewed: No visto
viewed_on_time: 'Visto el %{time}'
declined_on_time: 'Rechazado el %{time}'
expire_on_time: 'Expira el %{time}'
sign_in_person: Firma en persona
@ -2205,15 +2249,19 @@ it: &it
thanks: Grazie
you_have_been_invited_to_submit_the_name_form: 'Sei stato invitato a inviare il modulo "%{name}".'
you_have_been_invited_to_sign_the_name: 'Sei stato invitato a firmare il "%{name}".'
you_have_been_invited_to_view_the_name: 'Sei stato invitato a visualizzare il "%{name}".'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: "In alternativa, puoi rivedere e scaricare la tua copia utilizzando il link qui sotto:"
please_check_the_copy_of_your_name_in_the_email_attachments: "Per favore, controlla la copia del tuo \"%{name}\" negli allegati dell'email."
review_and_sign: Rivedi e Firma
view_document: Visualizza documento
review_and_submit: Rivedi e Invia
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Per favore, contattaci rispondendo a questa email se hai domande."
submitter_invitation_sms_body_sign: '{account.name} ti ha invitato a firmare un documento: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} ti ha invitato a visualizzare un documento: {submitter.link}'
verification_code_sms_body: 'Codice di verifica: {code}'
you_are_invited_to_submit_a_form: 'Sei stato invitato a inviare un modulo'
you_are_invited_to_sign_a_document: 'Sei stato invitato a firmare un documento'
you_are_invited_to_view_a_document: 'Sei stato invitato a visualizzare un documento'
you_are_invited_to_sign_documents: 'Sei stato invitato a firmare dei documenti'
your_document_copy: 'La tua copia del documento'
name_has_been_completed_by_submitters: '"%{name}" è stato completato da %{submitters}.'
@ -2227,6 +2275,17 @@ it: &it
Per favore, contattaci rispondendo a questa email se hai domande.
Grazie,
{account.name}
submitter_invitation_email_view_body: |
Ciao,
Sei stato invitato a visualizzare il "{template.name}".
[Visualizza documento]({submitter.link})
Per favore, contattaci rispondendo a questa email se hai domande.
Grazie,
{account.name}
submitter_completed_email_body: |
@ -2418,6 +2477,8 @@ it: &it
invalid_timeserver: Server di timestamp non valido
email_templates: Modelli email
signature_request_email: Email di richiesta di firma
view_documents_email: Email di visualizzazione documenti
view_only: Sola lettura
signature_request_reminder_email: Email di promemoria di richiesta di firma
signature_request_sms: SMS di richiesta di firma
verification_code_sms: SMS con codice di verifica
@ -2680,6 +2741,7 @@ it: &it
form_has_been_archived: Il modulo è stato archiviato.
form_has_been_expired: Il modulo è scaduto.
form_has_been_declined: Il modulo è stato rifiutato.
form_is_view_only: Il modulo è di sola lettura.
file_is_missing: File mancante
folder_name_has_been_updated: Il nome della cartella è stato aggiornato.
unable_to_rename_folder: Impossibile rinominare la cartella.
@ -2764,6 +2826,10 @@ it: &it
signers: Firmatari
not_invited_yet: Non ancora invitato
not_completed_yet: Non ancora completato
not_completed: Non completato
not_viewed_yet: Non ancora visualizzato
not_viewed: Non visualizzato
viewed_on_time: 'Visualizzato il %{time}'
declined_on_time: 'Rifiutato il %{time}'
expire_on_time: 'Scade il %{time}'
sign_in_person: Firma di persona
@ -3259,15 +3325,19 @@ fr: &fr
thanks: Merci
you_have_been_invited_to_submit_the_name_form: Vous avez été invité à soumettre le formulaire "%{name}".
you_have_been_invited_to_sign_the_name: Vous avez été invité à signer "%{name}".
you_have_been_invited_to_view_the_name: Vous avez été invité à consulter "%{name}".
alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'Vous pouvez également consulter et télécharger votre exemplaire à laide du lien cidessous :'
please_check_the_copy_of_your_name_in_the_email_attachments: Veuillez vérifier la copie de votre "%{name}" dans les pièces jointes de lemail.
review_and_sign: Examiner et signer
view_document: Consulter le document
review_and_submit: Examiner et soumettre
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: Veuillez nous contacter en répondant à cet email si vous avez des questions.
submitter_invitation_sms_body_sign: "{account.name} vous a invité à signer un document : {submitter.link}"
submitter_invitation_sms_body_view: "{account.name} vous a invité à consulter un document : {submitter.link}"
verification_code_sms_body: 'Code de vérification : {code}'
you_are_invited_to_submit_a_form: Vous êtes invité à soumettre un formulaire
you_are_invited_to_sign_a_document: Vous êtes invité à signer un document
you_are_invited_to_view_a_document: Vous êtes invité à consulter un document
you_are_invited_to_sign_documents: Vous êtes invité à signer des documents
your_document_copy: Votre copie de document
name_has_been_completed_by_submitters: '"%{name}" a été complété par %{submitters}.'
@ -3281,6 +3351,17 @@ fr: &fr
Veuillez nous contacter en répondant à cet email si vous avez des questions.
Merci,
{account.name}
submitter_invitation_email_view_body: |
Bonjour,
Vous avez été invité à consulter "{template.name}".
[Consulter le document]({submitter.link})
Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.
Merci,
{account.name}
submitter_completed_email_body: |
@ -3472,6 +3553,8 @@ fr: &fr
invalid_timeserver: Serveur dhorodatage invalide
email_templates: Modèles demail
signature_request_email: Email de demande de signature
view_documents_email: E-mail de consultation des documents
view_only: Lecture seule
signature_request_reminder_email: Email de rappel de demande de signature
signature_request_sms: SMS de demande de signature
verification_code_sms: SMS de code de vérification
@ -3734,6 +3817,7 @@ fr: &fr
form_has_been_archived: Le formulaire a été archivé.
form_has_been_expired: Le formulaire a expiré.
form_has_been_declined: Le formulaire a été refusé.
form_is_view_only: Le formulaire est en lecture seule.
file_is_missing: Fichier manquant
folder_name_has_been_updated: Le nom du dossier a été mis à jour.
unable_to_rename_folder: Impossible de renommer le dossier.
@ -3818,6 +3902,10 @@ fr: &fr
signers: Signataires
not_invited_yet: Pas encore invité
not_completed_yet: Pas encore terminé
not_completed: Non terminé
not_viewed_yet: Pas encore consulté
not_viewed: Non consulté
viewed_on_time: Consulté le %{time}
declined_on_time: Refusé le %{time}
expire_on_time: Expire le %{time}
sign_in_person: Signer en personne
@ -4310,15 +4398,19 @@ pt: &pt
thanks: Obrigado
you_have_been_invited_to_submit_the_name_form: 'Você foi convidado a submeter o formulário "%{name}".'
you_have_been_invited_to_sign_the_name: 'Você foi convidado a assinar "%{name}".'
you_have_been_invited_to_view_the_name: 'Você foi convidado a visualizar "%{name}".'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'Você pode revisar e baixar sua cópia usando o link abaixo:'
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, verifique a cópia de "%{name}" nos anexos do e-mail.'
review_and_sign: Revisar e assinar
view_document: Visualizar documento
review_and_submit: Revisar e submeter
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.'
submitter_invitation_sms_body_sign: '{account.name} convidou você para assinar um documento: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} convidou você para visualizar um documento: {submitter.link}'
verification_code_sms_body: 'Código de verificação: {code}'
you_are_invited_to_submit_a_form: Você foi convidado a submeter um formulário
you_are_invited_to_sign_a_document: Você foi convidado a assinar um documento
you_are_invited_to_view_a_document: Você foi convidado a visualizar um documento
you_are_invited_to_sign_documents: Você foi convidado a assinar documentos
your_document_copy: Sua cópia do documento
name_has_been_completed_by_submitters: '"%{name}" foi concluído por %{submitters}.'
@ -4332,6 +4424,17 @@ pt: &pt
Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.
Obrigado,
{account.name}
submitter_invitation_email_view_body: |
Olá,
Você foi convidado a visualizar "{template.name}".
[Visualizar documento]({submitter.link})
Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.
Obrigado,
{account.name}
submitter_completed_email_body: |
@ -4523,6 +4626,8 @@ pt: &pt
invalid_timeserver: Servidor de carimbo de tempo inválido
email_templates: Modelos de e-mail
signature_request_email: E-mail de solicitação de assinatura
view_documents_email: E-mail de visualização de documentos
view_only: Somente leitura
signature_request_reminder_email: E-mail de lembrete de solicitação de assinatura
signature_request_sms: SMS de solicitação de assinatura
verification_code_sms: SMS com código de verificação
@ -4785,6 +4890,7 @@ pt: &pt
form_has_been_archived: O formulário foi arquivado.
form_has_been_expired: O formulário expirou.
form_has_been_declined: O formulário foi recusado.
form_is_view_only: O formulário é somente leitura.
file_is_missing: O arquivo está ausente
folder_name_has_been_updated: O nome da pasta foi atualizado.
unable_to_rename_folder: Não foi possível renomear a pasta.
@ -4869,6 +4975,10 @@ pt: &pt
signers: Signatários
not_invited_yet: Ainda não convidado
not_completed_yet: Ainda não concluído
not_completed: Não concluído
not_viewed_yet: Ainda não visualizado
not_viewed: Não visualizado
viewed_on_time: 'Visualizado em %{time}'
declined_on_time: 'Recusado em %{time}'
expire_on_time: 'Expira em %{time}'
sign_in_person: Assinar pessoalmente
@ -5362,16 +5472,20 @@ de: &de
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfsumme wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren.
you_have_been_invited_to_submit_the_name_form: 'Sie wurden eingeladen, das Formular "%{name}" einzureichen.'
you_have_been_invited_to_sign_the_name: 'Sie wurden eingeladen, "%{name}" zu unterschreiben.'
you_have_been_invited_to_view_the_name: 'Sie wurden eingeladen, "%{name}" anzusehen.'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'Alternativ können Sie Ihre Kopie über den untenstehenden Link ansehen und herunterladen:'
please_check_the_copy_of_your_name_in_the_email_attachments: 'Bitte prüfen Sie die Kopie von "%{name}" im E-Mail-Anhang.'
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
review_and_sign: Prüfen und unterschreiben
view_document: Dokument ansehen
review_and_submit: Prüfen und einreichen
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.'
submitter_invitation_sms_body_sign: '{account.name} hat Sie eingeladen, ein Dokument zu unterschreiben: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} hat Sie eingeladen, ein Dokument anzusehen: {submitter.link}'
verification_code_sms_body: 'Verifizierungscode: {code}'
you_are_invited_to_submit_a_form: Sie sind eingeladen, ein Formular einzureichen
you_are_invited_to_sign_a_document: Sie sind eingeladen, ein Dokument zu unterschreiben
you_are_invited_to_view_a_document: Sie sind eingeladen, ein Dokument anzusehen
you_are_invited_to_sign_documents: Sie sind eingeladen, Dokumente zu unterschreiben
your_document_copy: Ihre Dokumentkopie
name_has_been_completed_by_submitters: '"%{name}" wurde von %{submitters} abgeschlossen.'
@ -5385,6 +5499,17 @@ de: &de
Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.
Danke,
{account.name}
submitter_invitation_email_view_body: |
Hallo,
Sie wurden eingeladen, "{template.name}" anzusehen.
[Dokument ansehen]({submitter.link})
Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.
Danke,
{account.name}
submitter_completed_email_body: |
@ -5577,6 +5702,8 @@ de: &de
invalid_timeserver: Ungültiger Zeitstempelserver
email_templates: E-Mail-Vorlagen
signature_request_email: E-Mail für Signaturanfrage
view_documents_email: E-Mail zur Dokumentenansicht
view_only: Nur Ansicht
signature_request_reminder_email: E-Mail-Erinnerung für Signaturanfrage
signature_request_sms: SMS für Signaturanfrage
verification_code_sms: SMS mit Verifizierungscode
@ -5839,6 +5966,7 @@ de: &de
form_has_been_archived: Das Formular wurde archiviert.
form_has_been_expired: Das Formular ist abgelaufen.
form_has_been_declined: Das Formular wurde abgelehnt.
form_is_view_only: Das Formular ist schreibgeschützt.
file_is_missing: Datei fehlt
folder_name_has_been_updated: Der Ordnername wurde aktualisiert.
unable_to_rename_folder: Der Ordner konnte nicht umbenannt werden.
@ -5923,6 +6051,10 @@ de: &de
signers: Unterzeichner
not_invited_yet: Noch nicht eingeladen
not_completed_yet: Noch nicht abgeschlossen
not_completed: Nicht abgeschlossen
not_viewed_yet: Noch nicht angesehen
not_viewed: Nicht angesehen
viewed_on_time: 'Angesehen am %{time}'
declined_on_time: 'Abgelehnt am %{time}'
expire_on_time: 'Läuft ab am %{time}'
sign_in_person: Vor Ort unterschreiben
@ -6821,16 +6953,20 @@ nl: &nl
: Onderteken documenten met een vertrouwd certificaat geleverd door DocuSeal. Uw documenten en gegevens worden nooit gedeeld met DocuSeal. PDF-checksum wordt verstrekt om een vertrouwde handtekening te genereren.
you_have_been_invited_to_submit_the_name_form: U bent uitgenodigd om het formulier "%{name}" in te dienen.
you_have_been_invited_to_sign_the_name: U bent uitgenodigd om "%{name}" te ondertekenen.
you_have_been_invited_to_view_the_name: U bent uitgenodigd om "%{name}" te bekijken.
alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'U kunt uw exemplaar ook bekijken en downloaden via de onderstaande link:'
please_check_the_copy_of_your_name_in_the_email_attachments: Controleer de kopie van uw "%{name}" in de e-mailbijlagen.
awaiting_completion_by_the_other_party: In afwachting van voltooiing door de andere partij
review_and_sign: Bekijken en ondertekenen
view_document: Document bekijken
review_and_submit: Bekijken en indienen
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: Neem contact met ons op door op deze e-mail te antwoorden als u vragen heeft.
submitter_invitation_sms_body_sign: "{account.name} heeft u uitgenodigd om een document te ondertekenen: {submitter.link}"
submitter_invitation_sms_body_view: "{account.name} heeft u uitgenodigd om een document te bekijken: {submitter.link}"
verification_code_sms_body: 'Verificatiecode: {code}'
you_are_invited_to_submit_a_form: U bent uitgenodigd om een formulier in te dienen
you_are_invited_to_sign_a_document: U bent uitgenodigd om een document te ondertekenen
you_are_invited_to_view_a_document: U bent uitgenodigd om een document te bekijken
you_are_invited_to_sign_documents: U bent uitgenodigd om documenten te ondertekenen
your_document_copy: Uw documentkopie
name_has_been_completed_by_submitters: '"%{name}" is voltooid door %{submitters}.'
@ -6844,6 +6980,17 @@ nl: &nl
Neem contact met ons op door op deze e-mail te antwoorden als u vragen heeft.
Bedankt,
{account.name}
submitter_invitation_email_view_body: |
Hallo,
U bent uitgenodigd om "{template.name}" te bekijken.
[Document bekijken]({submitter.link})
Neem contact met ons op door op deze e-mail te antwoorden als u vragen heeft.
Bedankt,
{account.name}
submitter_completed_email_body: |
@ -7036,6 +7183,8 @@ nl: &nl
invalid_timeserver: Ongeldige tijdserver
email_templates: E-mailsjablonen
signature_request_email: E-mail voor handtekeningverzoek
view_documents_email: E-mail voor documentweergave
view_only: Alleen bekijken
signature_request_reminder_email: E-mailherinnering voor handtekeningverzoek
signature_request_sms: SMS voor handtekeningverzoek
verification_code_sms: Verificatiecode-SMS
@ -7298,6 +7447,7 @@ nl: &nl
form_has_been_archived: Formulier is gearchiveerd.
form_has_been_expired: Formulier is verlopen.
form_has_been_declined: Formulier is geweigerd.
form_is_view_only: Formulier is alleen-lezen.
file_is_missing: Bestand ontbreekt
folder_name_has_been_updated: Mapnaam is bijgewerkt.
unable_to_rename_folder: Kan map niet hernoemen.
@ -7382,6 +7532,10 @@ nl: &nl
signers: Ondertekenaars
not_invited_yet: Nog niet uitgenodigd
not_completed_yet: Nog niet voltooid
not_completed: Niet voltooid
not_viewed_yet: Nog niet bekeken
not_viewed: Niet bekeken
viewed_on_time: Bekeken op %{time}
declined_on_time: Geweigerd op %{time}
expire_on_time: Verloopt op %{time}
sign_in_person: In persoon ondertekenen

@ -6,7 +6,10 @@ module Submissions
module_function
def maybe_update_completed_at(submission)
incomplete_submitter = Submitter.where(submission_id: submission.id, completed_at: nil).select(1)
viewer_uuids = submission.template_submitters.to_a.filter_map { |s| s['uuid'] if s['is_viewer'] }
incomplete_submitter = Submitter.where(submission_id: submission.id, completed_at: nil).limit(1)
incomplete_submitter = incomplete_submitter.where.not(uuid: viewer_uuids) if viewer_uuids.present?
max_completed_at =
Arel::Nodes::Grouping.new(
@ -16,7 +19,7 @@ module Submissions
)
Submission.where(id: submission.id, completed_at: nil)
.where.not(incomplete_submitter.arel.exists)
.where.not(incomplete_submitter.select(1).arel.exists)
.update_all(completed_at: max_completed_at)
.positive?
end
@ -124,6 +127,8 @@ module Submissions
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submission)
Submissions::CreateFromSubmitters.assign_submitters_is_viewer(submission)
submission.save!
if submission.expire_at?
@ -163,15 +168,29 @@ module Submissions
Submitters.send_signature_requests(first_submitters, delay_seconds:)
elsif submission.submitters_order_preserved?
first_submitter = template_submitters.filter_map { |s| submitters_index[s['uuid']] }.first
first_submitter = template_submitters.filter_map { |s| submitters_index[s['uuid']] }.find { |s| !s.viewer? }
if first_submitter
first_viewers = find_first_viewers(first_submitter, submitters_index)
Submitters.send_signature_requests([first_submitter], delay_seconds:) if first_submitter
Submitters.send_signature_requests([first_submitter, *first_viewers], delay_seconds:)
end
else
Submitters.send_signature_requests(submitters_index.values, delay_seconds:)
end
end
end
def find_first_viewers(first_submitter, submitters_index)
first_submitter.submission.template_submitters.each_with_object([]) do |s, viewers|
submitter = submitters_index[s['uuid']]
break viewers if submitter && !submitter.viewer? && submitter != first_submitter
viewers << submitter if submitter&.viewer?
end
end
def normalize_email(email)
return if email.blank?
return if email.is_a?(Numeric)

@ -10,6 +10,8 @@ module Submissions
assign_defined_submitters(submission)
assign_linked_submitters(submission)
Submissions::CreateFromSubmitters.assign_submitters_is_viewer(submission)
submission
end

@ -90,6 +90,8 @@ module Submissions
maybe_add_invite_submitters(submission, template, attrs[:submitters])
assign_submitters_is_viewer(submission)
submission.template = nil unless with_template
submission.tap(&:save!)
@ -100,6 +102,34 @@ module Submissions
submissions
end
def assign_submitters_is_viewer(submission)
template_fields = submission.template_fields || submission.template.fields
field_submitter_uuids = Set.new(template_fields.pluck('submitter_uuid'))
viewer_template_submitters =
submission.template_submitters.select { |s| field_submitter_uuids.exclude?(s['uuid']) }
return submission if viewer_template_submitters.blank?
viewer_template_submitters.each { |s| s['is_viewer'] = true }
return submission if submission.template_submitters.any? { |s| s['order'] }
first_submitter = submission.submitters.find(&:sent_at)
return submission unless first_submitter
submitters_index = submission.submitters.reject(&:completed_at?).index_by(&:uuid)
Submissions.find_first_viewers(first_submitter, submitters_index).each do |viewer|
next if viewer.preferences['send_email'] == false || viewer.email.blank?
viewer.sent_at ||= first_submitter.sent_at
end
submission
end
def maybe_set_dynamic_documents(submission)
return submission unless submission.template_id?

@ -282,6 +282,9 @@ module Submissions
[
composer.document.layout.formatted_text_box(
[
submitter.viewer? && {
text: "#{I18n.t('view_only')}\n"
},
submitter.email && (click_email_event || verify_email_event) && {
text: "#{I18n.t('email_verification')}: #{I18n.t('verified')}\n"
},

@ -996,7 +996,8 @@ module Submissions
end
def single_sign_reason(submitter)
signers = submitter.submission.submitters.sort_by(&:completed_at).map { |s| s.email || s.name || s.phone }
signers = submitter.submission.submitters.reject(&:viewer?)
.sort_by(&:completed_at).map { |s| s.email || s.name || s.phone }
format(SIGN_REASON, name: signers.reverse.join(', '))
end
@ -1015,7 +1016,7 @@ module Submissions
return sign_reason(reason_name) if config.value == 'multiple'
if !submitter.submission.submitters.exists?(completed_at: nil) &&
if submitter.submission.completed_at? &&
submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
return single_sign_reason(submitter)
end

@ -177,6 +177,7 @@ module Submitters
end
end
# rubocop:disable Metrics
def current_submitter_order?(submitter)
submission = submitter.submission
@ -189,6 +190,14 @@ module Submitters
current_group_index = submitter_groups.find_index { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }
submitter_groups.first(current_group_index).flat_map(&:last)
elsif submitter.viewer?
current_index = submitter_items.find_index { |e| e['uuid'] == submitter.uuid }
preceding_submitter_index = submitter_items[0...current_index].rindex do |e|
!submission.submitters.find { |s| s.uuid == e['uuid'] }&.viewer?
end
submitter_items.first(preceding_submitter_index || 0)
else
submitter_items.first(submitter_items.find_index { |e| e['uuid'] == submitter.uuid })
end
@ -196,9 +205,10 @@ module Submitters
before_items.all? do |item|
submitter = submission.submitters.find { |e| e.uuid == item['uuid'] }
submitter.nil? || submitter.completed_at?
submitter.nil? || submitter.viewer? || submitter.completed_at?
end
end
# rubocop:enable Metrics
def build_document_filename(submitter, blob, filename_format)
return blob.filename.to_s if filename_format.blank?

@ -0,0 +1,25 @@
# frozen_string_literal: true
describe 'Submit Form' do
let(:account) { create(:account) }
let(:author) { create(:user, account:) }
let(:template) { create(:template, account:, author:, submitter_count: 2, only_field_types: %w[text]) }
let(:viewer_uuid) { template.submitters.second['uuid'] }
let(:submission) do
create(:submission, :with_submitters, template:).tap do |s|
s.update!(template_submitters: s.template_submitters.map do |ts|
ts['uuid'] == viewer_uuid ? ts.merge('is_viewer' => true) : ts
end)
end
end
let(:viewer) { submission.submitters.find { |e| e.uuid == viewer_uuid } }
describe 'PUT /s/:slug' do
it 'blocks form submission for a view-only party' do
put submit_form_path(slug: viewer.slug), params: { completed: 'true' }
expect(response).to have_http_status(:unprocessable_content)
expect(response.parsed_body['error']).to eq(I18n.t('form_is_view_only'))
end
end
end

@ -259,6 +259,35 @@ describe 'Submission API' do
end
end
describe 'view-only (CC) party' do
let(:viewer_template) { create(:template, account:, author:, submitter_count: 2, only_field_types: %w[text]) }
let(:viewer_uuid) { viewer_template.submitters.second['uuid'] }
before do
viewer_template.update!(fields: viewer_template.fields.reject { |f| f['submitter_uuid'] == viewer_uuid })
end
it 'stamps the field-less party as view-only on POST /api/submissions' do
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
template_id: viewer_template.id,
submitters: [
{ role: 'First Party', email: 'signer@example.com' },
{ role: 'Second Party', email: 'viewer@example.com' }
]
}.to_json
expect(response).to have_http_status(:ok)
submission = Submission.last
viewer = submission.submitters.find { |e| e.uuid == viewer_uuid }
signer = submission.submitters.find { |e| e.uuid != viewer_uuid }
expect(viewer.viewer?).to be(true)
expect(signer.viewer?).to be(false)
expect(submission.template_submitters.find { |e| e['uuid'] == viewer_uuid }['is_viewer']).to be(true)
end
end
private
def index_submission_body(submission)

@ -1317,4 +1317,27 @@ RSpec.describe 'Signing Form' do
expect(field_value(submitter, 'First Name')).to eq 'John Doe'
end
end
context 'when a view-only party opens the form' do
let(:template) { create(:template, account:, author:, submitter_count: 2, only_field_types: %w[text]) }
let(:viewer_uuid) { template.submitters.second['uuid'] }
let(:submission) do
create(:submission, template:).tap do |s|
s.update!(
template_fields: s.template_fields.reject { |f| f['submitter_uuid'] == viewer_uuid },
template_submitters: s.template_submitters.map do |ts|
ts['uuid'] == viewer_uuid ? ts.merge('is_viewer' => true) : ts
end
)
end
end
let(:viewer) { create(:submitter, submission:, uuid: viewer_uuid, account:, email: 'viewer@example.com') }
it 'opens the document form read-only for a view-only party' do
visit submit_form_path(slug: viewer.slug)
expect(page).to have_content(template.name)
expect(page).to have_no_css('#form_container')
end
end
end

Loading…
Cancel
Save