+ <% end %>
<%= render 'templates_preferences/submitter_invitation_reminder_email_collapse' %>
diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml
index 684c5ebb..078e4f47 100644
--- a/config/locales/i18n.yml
+++ b/config/locales/i18n.yml
@@ -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
@@ -877,6 +899,7 @@ en: &en
mobile: Mobile
tablet: Tablet
reset_default: Reset default
+ smtp_settings_have_been_reset: SMTP settings have been reset.
send_signature_request_email: Send signature request email
last_3_months: Last 3 months
last_6_months: Last 6 months
@@ -1151,15 +1174,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 +1200,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 +1402,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 +1666,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 +1751,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
@@ -1931,6 +1976,7 @@ es: &es
mobile: Móvil
tablet: Tableta
reset_default: Restablecer por defecto
+ smtp_settings_have_been_reset: La configuración SMTP ha sido restablecida.
send_signature_request_email: Enviar correo de solicitud de firma
last_3_months: Últimos 3 meses
last_6_months: Últimos 6 meses
@@ -2205,15 +2251,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 +2277,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 +2479,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 +2743,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 +2828,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
@@ -2985,6 +3053,7 @@ it: &it
mobile: Mobile
tablet: Tablet
reset_default: Reimposta predefinito
+ smtp_settings_have_been_reset: Le impostazioni SMTP sono state ripristinate.
send_signature_request_email: Invia email di richiesta firma
last_3_months: Ultimi 3 mesi
last_6_months: Ultimi 6 mesi
@@ -3259,15 +3328,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 à l’aide du lien ci‑dessous :'
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 l’e‑mail.
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 e‑mail 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 +3354,17 @@ fr: &fr
Veuillez nous contacter en répondant à cet e‑mail 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 +3556,8 @@ fr: &fr
invalid_timeserver: Serveur d’horodatage invalide
email_templates: Modèles d’e‑mail
signature_request_email: E‑mail de demande de signature
+ view_documents_email: E-mail de consultation des documents
+ view_only: Lecture seule
signature_request_reminder_email: E‑mail 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 +3820,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 +3905,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
@@ -4035,6 +4126,7 @@ fr: &fr
mobile: Mobile
tablet: Tablette
reset_default: Réinitialiser par défaut
+ smtp_settings_have_been_reset: Les paramètres SMTP ont été réinitialisés.
send_signature_request_email: Envoyer un e-mail de demande de signature
last_month: Mois dernier
last_3_months: 3 derniers mois
@@ -4310,15 +4402,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 +4428,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 +4630,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 +4894,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 +4979,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
@@ -5090,6 +5204,7 @@ pt: &pt
mobile: Celular
tablet: Tablet
reset_default: Redefinir para padrão
+ smtp_settings_have_been_reset: As configurações SMTP foram redefinidas.
send_signature_request_email: Enviar e-mail de solicitação de assinatura
last_3_months: Últimos 3 meses
last_6_months: Últimos 6 meses
@@ -5362,16 +5477,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 +5504,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 +5707,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 +5971,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 +6056,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
@@ -6144,6 +6281,7 @@ de: &de
mobile: Mobil
tablet: Tablet
reset_default: Standard zurücksetzen
+ smtp_settings_have_been_reset: Die SMTP-Einstellungen wurden zurückgesetzt.
send_signature_request_email: Signaturanfrage-E-Mail senden
last_3_months: Letzte 3 Monate
last_6_months: Letzte 6 Monate
@@ -6821,16 +6959,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 +6986,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 +7189,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 +7453,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 +7538,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
@@ -7599,6 +7759,7 @@ nl: &nl
mobile: Mobiel
tablet: Tablet
reset_default: Standaard herstellen
+ smtp_settings_have_been_reset: De SMTP-instellingen zijn hersteld.
send_signature_request_email: E-mail met handtekeningaanvraag verzenden
last_3_months: Afgelopen 3 maanden
last_6_months: Afgelopen 6 maanden
diff --git a/config/routes.rb b/config/routes.rb
index e1100e04..b31db369 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -29,7 +29,7 @@ Rails.application.routes.draw do
resources :submitter_email_clicks, only: %i[create]
resources :submitter_form_views, only: %i[create]
resources :submitters, only: %i[index show update]
- resources :submissions, only: %i[index show create destroy] do
+ resources :submissions, only: %i[index show create update destroy] do
resources :documents, only: %i[index], controller: 'submission_documents'
collection do
resources :init, only: %i[create], controller: 'submissions'
@@ -189,7 +189,7 @@ Rails.application.routes.draw do
resources :api, only: %i[index create], controller: 'api_settings'
resource :reveal_access_token, only: %i[show create], controller: 'reveal_access_token'
end
- resources :email, only: %i[index create], controller: 'email_smtp_settings'
+ resources :email, only: %i[index create destroy], controller: 'email_smtp_settings'
resources :sso, only: %i[index], controller: 'sso_settings'
resources :notifications, only: %i[index create], controller: 'notifications_settings'
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
diff --git a/db/migrate/20260707055354_add_submission_created_at_index.rb b/db/migrate/20260707055354_add_submission_created_at_index.rb
new file mode 100644
index 00000000..2b68d6ec
--- /dev/null
+++ b/db/migrate/20260707055354_add_submission_created_at_index.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddSubmissionCreatedAtIndex < ActiveRecord::Migration[8.1]
+ def change
+ add_index :submissions, :created_at, if_not_exists: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 71023267..7e7ca23e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.1].define(version: 2026_07_01_165617) do
+ActiveRecord::Schema[8.1].define(version: 2026_07_07_055354) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql"
@@ -379,6 +379,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_01_165617) do
t.index ["account_id", "id"], name: "index_submissions_on_account_id_and_id_pending", where: "((completed_at IS NULL) AND (archived_at IS NULL))"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id", where: "(archived_at IS NULL)"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id_archived", where: "(archived_at IS NOT NULL)"
+ t.index ["created_at"], name: "index_submissions_on_created_at"
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
t.index ["slug"], name: "index_submissions_on_slug", unique: true
t.index ["template_id"], name: "index_submissions_on_template_id"
diff --git a/lib/email_messages.rb b/lib/email_messages.rb
index 08436730..eed9d0a5 100644
--- a/lib/email_messages.rb
+++ b/lib/email_messages.rb
@@ -8,9 +8,14 @@ module EmailMessages
ASSET_REGEXP = Regexp.union(STYLE_REGEXP, BASE64_REGEXP)
ASSET_PREFIX = '[[asset:'
PLACEHOLDER_REGEXP = /\[\[asset:(\h{40})\]\]/
+ HTML_MIME_TYPES = ['text/html', 'application/xhtml+xml'].freeze
module_function
+ def html_body?(content)
+ content.present? && HTML_MIME_TYPES.include?(Marcel::MimeType.for(content.dup))
+ end
+
def find_or_create_for_account_user(account, user, subject, body)
subject = I18n.t(:you_are_invited_to_sign_a_document) if subject.blank?
diff --git a/lib/submissions.rb b/lib/submissions.rb
index 323dc317..08a44e42 100644
--- a/lib/submissions.rb
+++ b/lib/submissions.rb
@@ -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,10 +127,13 @@ module Submissions
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submission)
+ Submissions::CreateFromSubmitters.assign_submitters_is_viewer(submission)
+
submission.save!
if submission.expire_at?
- ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id)
+ ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id,
+ 'expire_at' => submission.expire_at.to_i)
end
submission
@@ -163,15 +169,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)
diff --git a/lib/submissions/assign_defined_submitters.rb b/lib/submissions/assign_defined_submitters.rb
index edbd26e5..54abaaa0 100644
--- a/lib/submissions/assign_defined_submitters.rb
+++ b/lib/submissions/assign_defined_submitters.rb
@@ -10,6 +10,8 @@ module Submissions
assign_defined_submitters(submission)
assign_linked_submitters(submission)
+ Submissions::CreateFromSubmitters.assign_submitters_is_viewer(submission)
+
submission
end
diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb
index 59479090..7332ce9b 100644
--- a/lib/submissions/create_from_submitters.rb
+++ b/lib/submissions/create_from_submitters.rb
@@ -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?
@@ -148,7 +178,8 @@ module Submissions
submissions.each do |submission|
next unless submission.expire_at?
- ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id)
+ ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id,
+ 'expire_at' => submission.expire_at.to_i)
end
end
diff --git a/lib/submissions/ensure_combined_generated.rb b/lib/submissions/ensure_combined_generated.rb
index 4b55a4b7..2eeb030b 100644
--- a/lib/submissions/ensure_combined_generated.rb
+++ b/lib/submissions/ensure_combined_generated.rb
@@ -15,7 +15,8 @@ module Submissions
def call(submitter)
return nil unless submitter
- raise NotCompletedYet unless submitter.completed_at?
+ raise NotCompletedYet if !submitter.completed_at? ||
+ !submitter.submission.completed_at?
total_wait_time ||= 0
key = [KEY_PREFIX, submitter.id].join(':')
diff --git a/lib/submissions/filter.rb b/lib/submissions/filter.rb
index 7da3f534..f338a877 100644
--- a/lib/submissions/filter.rb
+++ b/lib/submissions/filter.rb
@@ -19,6 +19,8 @@ module Submissions
created_at_to
].freeze
+ BIGINT_MAX = (2**63) - 1
+
module_function
def call(submissions, current_user, params)
@@ -71,15 +73,39 @@ module Submissions
end
def filter_by_created_at(submissions, filters)
- submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present?
+ if filters[:created_at_from].present?
+ submissions = submissions.where(min_created_at_id_arel(filters[:created_at_from]))
+ end
if filters[:created_at_to].present?
- submissions = submissions.where(created_at: ..filters[:created_at_to].end_of_day)
+ submissions = submissions.where(max_created_at_id_arel(filters[:created_at_to].end_of_day))
end
submissions
end
+ def min_created_at_id_arel(time)
+ submissions = Submission.arel_table
+
+ first_id = submissions.project(submissions[:id])
+ .where(submissions[:created_at].gteq(time))
+ .order(submissions[:created_at].asc, submissions[:id].asc)
+ .take(1)
+
+ submissions[:id].gteq(Arel::Nodes::NamedFunction.new('COALESCE', [first_id, BIGINT_MAX]))
+ end
+
+ def max_created_at_id_arel(time)
+ submissions = Submission.arel_table
+
+ last_id = submissions.project(submissions[:id])
+ .where(submissions[:created_at].lteq(time))
+ .order(submissions[:created_at].desc, submissions[:id].desc)
+ .take(1)
+
+ submissions[:id].lteq(Arel::Nodes::NamedFunction.new('COALESCE', [last_id, 0]))
+ end
+
def filter_by_folder(submissions, filters, current_user)
return submissions if filters[:folder].blank?
diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb
index 35ed789e..d7e9f6ea 100644
--- a/lib/submissions/generate_audit_trail.rb
+++ b/lib/submissions/generate_audit_trail.rb
@@ -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"
},
diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb
index e9964400..850b7908 100644
--- a/lib/submissions/generate_result_attachments.rb
+++ b/lib/submissions/generate_result_attachments.rb
@@ -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
diff --git a/lib/submitters.rb b/lib/submitters.rb
index 5499538d..4783d404 100644
--- a/lib/submitters.rb
+++ b/lib/submitters.rb
@@ -84,7 +84,7 @@ module Submitters
submitter_ids = SearchEntry.where(record_type: 'Submitter')
.where(account_id: current_user.account_id)
.where(*query)
- .limit(keyword.length > 2 ? 500 : 5000)
+ .limit(keyword.strip.length > 2 ? 500 : 5000)
.pluck(:record_id)
submitters.where(id: submitter_ids.first(100))
@@ -105,7 +105,7 @@ module Submitters
end
def select_attachments_for_download(submitter)
- if AccountConfig.exists?(account_id: submitter.submission.account_id,
+ if AccountConfig.exists?(account_id: submitter.account_id,
key: AccountConfig::COMBINE_PDF_RESULT_KEY,
value: true) &&
submitter.submission.completed_at? &&
@@ -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?
@@ -219,10 +229,12 @@ module Submitters
end
end
- filename = filename.gsub(
- '{submission.completed_at}',
- I18n.l(submitter.completed_at.in_time_zone(submitter.account.timezone), format: :short)
- )
+ filename = filename.gsub('{submission.completed_at}') do
+ completed_at = submitter.submission.completed_at ||
+ submitter.submission.submitters.select(&:completed_at).max_by(&:completed_at).completed_at
+
+ I18n.l(completed_at.in_time_zone(submitter.account.timezone), format: :short)
+ end
"#{filename}.#{blob.filename.extension}"
end
@@ -265,7 +277,7 @@ module Submitters
def build_combined_url(submitter, ttl: FILES_TTL)
return unless submitter.submission.completed_at?
- return if submitter.submission.submitters.order(:completed_at).last != submitter
+ return if submitter.submission.submitters.completed.order(:completed_at).last != submitter
attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
diff --git a/lib/submitters/generate_font_image.rb b/lib/submitters/generate_font_image.rb
index 0413355f..05e7e454 100644
--- a/lib/submitters/generate_font_image.rb
+++ b/lib/submitters/generate_font_image.rb
@@ -4,14 +4,15 @@ module Submitters
module GenerateFontImage
WIDTH = 3000
HEIGHT = 160
+ SHEAR = 0.25
FONTS = {
'Dancing Script Regular' => '/fonts/DancingScript-Regular.otf',
- 'Go Noto Kurrent-Bold Bold' => '/fonts/GoNotoKurrent-Bold.ttf'
+ 'Go Noto Kurrent-Regular Regular' => '/fonts/GoNotoKurrent-Regular.ttf'
}.freeze
FONT_ALIASES = {
- 'initials' => 'Go Noto Kurrent-Bold Bold',
+ 'initials' => 'Go Noto Kurrent-Regular Regular',
'signature' => 'Dancing Script Regular'
}.freeze
@@ -25,6 +26,10 @@ module Submitters
text_image = Vips::Image.text(text, font:, fontfile: FONTS[font],
width: WIDTH, height: HEIGHT, wrap: :none)
+ text_image = text_image.affine([1, -SHEAR, 0, 1], background: [0])
+
+ text_image = text_image.crop(*text_image.find_trim(background: [0], threshold: 0))
+
text_mask = Vips::Image.black(text_image.width, text_image.height)
image = text_mask.bandjoin(text_image).copy(interpretation: :b_w)
diff --git a/lib/submitters/normalize_values.rb b/lib/submitters/normalize_values.rb
index a2c99ac5..2162e970 100644
--- a/lib/submitters/normalize_values.rb
+++ b/lib/submitters/normalize_values.rb
@@ -305,7 +305,13 @@ module Submitters
return unless blob
- return blob if blob.attachments.take&.record&.account_id == account.id
+ attachment = blob.attachments.take
+
+ return if attachment.nil? ||
+ attachment.record_type != 'Submitter' ||
+ attachment.name != 'attachments'
+
+ return blob if attachment.record&.account_id == account.id
nil
end
diff --git a/lib/templates.rb b/lib/templates.rb
index 26e58a3e..44ae5cae 100644
--- a/lib/templates.rb
+++ b/lib/templates.rb
@@ -59,6 +59,7 @@ module Templates
.select(1).arel.exists
Template.where(id: TemplateSharing.where(account_id: shared_account_ids).select(:template_id))
+ .where(account_id: account.linked_account_account.account_id)
.where.not(exists_access)
end
diff --git a/spec/jobs/process_submission_expired_job_spec.rb b/spec/jobs/process_submission_expired_job_spec.rb
new file mode 100644
index 00000000..bd638de0
--- /dev/null
+++ b/spec/jobs/process_submission_expired_job_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+RSpec.describe ProcessSubmissionExpiredJob do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account:) }
+ let(:template) { create(:template, account:, author: user) }
+ let(:expire_at) { 2.days.from_now.change(usec: 0) }
+ let(:submission) do
+ create(:submission, :with_submitters, template:, created_by_user: user, expire_at:)
+ end
+
+ before { allow(WebhookUrls).to receive(:enqueue_events) }
+
+ describe '#perform' do
+ it 'enqueues the expired event when the scheduled expire_at still matches' do
+ described_class.new.perform('submission_id' => submission.id, 'expire_at' => expire_at.to_i)
+
+ expect(WebhookUrls).to have_received(:enqueue_events).with(submission, 'submission.expired')
+ end
+
+ it 'enqueues the expired event for legacy jobs scheduled without an expire_at param' do
+ described_class.new.perform('submission_id' => submission.id)
+
+ expect(WebhookUrls).to have_received(:enqueue_events).with(submission, 'submission.expired')
+ end
+
+ it 'skips a stale job scheduled for an earlier expire_at that was extended' do
+ submission.update!(expire_at: 3.days.from_now)
+
+ described_class.new.perform('submission_id' => submission.id, 'expire_at' => expire_at.to_i)
+
+ expect(WebhookUrls).not_to have_received(:enqueue_events)
+ end
+
+ it 'skips a stale job scheduled for a later expire_at that was shortened' do
+ submission.update!(expire_at: 1.day.from_now)
+
+ described_class.new.perform('submission_id' => submission.id, 'expire_at' => expire_at.to_i)
+
+ expect(WebhookUrls).not_to have_received(:enqueue_events)
+ end
+
+ it 'skips a stale job when the expiration has been cleared' do
+ submission.update!(expire_at: nil)
+
+ described_class.new.perform('submission_id' => submission.id, 'expire_at' => expire_at.to_i)
+
+ expect(WebhookUrls).not_to have_received(:enqueue_events)
+ end
+ end
+end
diff --git a/spec/requests/forms_spec.rb b/spec/requests/forms_spec.rb
new file mode 100644
index 00000000..8dcdf0fb
--- /dev/null
+++ b/spec/requests/forms_spec.rb
@@ -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
diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb
index 82698613..3adf6092 100644
--- a/spec/requests/submissions_spec.rb
+++ b/spec/requests/submissions_spec.rb
@@ -259,6 +259,91 @@ describe 'Submission API' do
end
end
+ describe 'PUT /api/submissions/:id' do
+ it 'updates the submission name' do
+ submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author)
+
+ put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
+ name: 'Updated Name'
+ }.to_json
+
+ expect(response).to have_http_status(:ok)
+ expect(submission.reload.name).to eq('Updated Name')
+ expect(response.parsed_body['name']).to eq('Updated Name')
+ end
+
+ it 'updates the expiration date' do
+ submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author)
+ expire_at = 1.week.from_now.change(usec: 0)
+
+ put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
+ expire_at: expire_at.iso8601
+ }.to_json
+
+ expect(response).to have_http_status(:ok)
+ expect(submission.reload.expire_at).to be_within(1.second).of(expire_at)
+ end
+
+ it 'clears the expiration date when passed nil' do
+ submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author,
+ expire_at: 1.week.from_now)
+
+ put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
+ expire_at: nil
+ }.to_json
+
+ expect(response).to have_http_status(:ok)
+ expect(submission.reload.expire_at).to be_nil
+ end
+
+ it 'archives and unarchives the submission' do
+ submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author)
+
+ put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
+ archived: true
+ }.to_json
+
+ expect(response).to have_http_status(:ok)
+ expect(submission.reload.archived_at).not_to be_nil
+
+ put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
+ archived: false
+ }.to_json
+
+ expect(response).to have_http_status(:ok)
+ expect(submission.reload.archived_at).to be_nil
+ 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)
diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb
index 15659626..0002dbc4 100644
--- a/spec/system/signing_form_spec.rb
+++ b/spec/system/signing_form_spec.rb
@@ -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