- <%= ff.label :request_email_body, t('email_body'), class: 'label' %>
-
+ <%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: @template.preferences['submitters'].to_a.size > 1 %>
+
+ <% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
+
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb
index 6d08ecaf..a37a59c0 100644
--- a/app/views/users/index.html.erb
+++ b/app/views/users/index.html.erb
@@ -60,7 +60,7 @@
- <%= user.last_sign_in_at ? l(user.last_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
+ <%= user.current_sign_in_at ? l(user.current_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
|
<% if params[:status].blank? && can?(:update, user) && user.archived_at.blank? %>
diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml
index 1e084319..74d5d9f2 100644
--- a/config/locales/i18n.yml
+++ b/config/locales/i18n.yml
@@ -20,10 +20,12 @@ en: &en
language_ko: 한국어
hi_there: Hi there
thanks: Thanks
+ edit_per_party: Edit per party
reply_to: Reply to
pending_by_me: Pending by me
partially_completed: Partially completed
unarchive: Unarchive
+ signed: Signed
first_party: 'First Party'
remove_filter: Remove filter
add: Add
@@ -41,6 +43,7 @@ en: &en
hello_name: Hello %{name}
you_are_invited_to_product_name: You are invited to %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:'
+ sent_using_product_name_in_testing_mode_html: 'Sent using %{product_name} in testing mode'
sent_using_product_name_free_document_signing_html: 'Sent using %{product_name} free document signing.'
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.'
@@ -164,7 +167,7 @@ en: &en
schedule_account_for_deletion_: Schedule account for deletion?
account_information_has_been_updated: Account information has been updated.
should_be_a_valid_url: should be a valid URL
- your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Your account removal request will be processed within 2 weeks. Please contact us if you want to keep your account.
+ your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Your account removal request will be processed within 2 months. Please contact us if you want to keep your account.
test_mode: Test mode
copy: Copy
copied: Copied
@@ -688,6 +691,22 @@ en: &en
tell_us_more_about_your_experience: Tell us more about your experience
extremely_dissatisfied: Extremely Dissatisfied
extremely_satisfied: Extremely Satisfied
+ your_pro_plan_payment_is_overdue: Your Pro plan payment is overdue.
+ click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: Click here to update your payment details and clear the invoice to ensure uninterrupted service.
+ overdue_payment: Overdue Payment
+ your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Your Pro Plan has been suspended due to unpaid invoices. You can update your payment details to settle the invoice and continue using DocuSeal or cancel your subscription.
+ manage_subscription: Manage Subscription
+ submission_created_by_email_html: 'Submission created by %{email}'
+ submission_created_by_email_via_source_html: 'Submission created by %{email} via %{source}'
+ submission_created_via_source_html: 'Submission created via %{source}'
+ pro_user_seats_used: Pro user seats used
+ manage_plan: Manage plan
+ submission_sources:
+ api: API
+ bulk: Bulk Send
+ embed: Embedding
+ invie: Invite
+ link: Link
submission_event_names:
send_email_to_html: 'Email sent to %{submitter_name}'
send_reminder_email_to_html: 'Reminder email sent to %{submitter_name}'
@@ -726,6 +745,8 @@ en: &en
read: Read your data
es: &es
+ edit_per_party: Editar por parte
+ signed: Firmado
reply_to: Responder a
partially_completed: Parcialmente completado
pending_by_me: Pendiente por mi
@@ -749,6 +770,7 @@ es: &es
hello_name: Hola %{name}
you_are_invited_to_product_name: Estás invitado a %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:'
+ sent_using_product_name_in_testing_mode_html: 'Enviado usando %{product_name} en Modo de Prueba'
sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de %{product_name}.'
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: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza.
hi_there: Hola
@@ -872,7 +894,7 @@ es: &es
schedule_account_for_deletion_: ¿Programar la eliminación de la cuenta?
account_information_has_been_updated: La información de la cuenta ha sido actualizada.
should_be_a_valid_url: debe ser una URL válida
- your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 semanas. Por favor contáctanos si deseas mantener tu cuenta.
+ your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 meses. Por favor contáctanos si deseas mantener tu cuenta.
test_mode: Modo de prueba
copy: Copiar
copied: Copiado
@@ -1396,6 +1418,22 @@ es: &es
tell_us_more_about_your_experience: Cuéntanos más sobre tu experiencia
extremely_dissatisfied: Extremadamente insatisfecho
extremely_satisfied: Extremadamente satisfecho
+ your_pro_plan_payment_is_overdue: El pago de tu plan Pro está atrasado.
+ click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Haz clic aquí para actualizar tus datos de pago y liquidar la factura para garantizar un servicio ininterrumpido.'
+ overdue_payment: Pago Atrasado
+ your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Tu plan Pro ha sido suspendido debido a facturas impagas. Puedes actualizar tus datos de pago para liquidar la factura y seguir usando DocuSeal o cancelar tu suscripción.
+ manage_subscription: Gestionar Suscripción
+ submission_created_by_email_html: 'Envío creado por %{email}'
+ submission_created_by_email_via_source_html: 'Envío creado por %{email} a través de %{source}'
+ submission_created_via_source_html: 'Envío creado a través de %{source}'
+ pro_user_seats_used: Plazas de usuario Pro en uso
+ manage_plan: Gestionar plan
+ submission_sources:
+ api: API
+ bulk: Envío masivo
+ embed: Integración
+ invite: Invitación
+ link: Enlace
submission_event_names:
send_email_to_html: 'Correo electrónico enviado a %{submitter_name}'
send_reminder_email_to_html: 'Correo de recordatorio enviado a %{submitter_name}'
@@ -1434,6 +1472,8 @@ es: &es
read: Leer tus datos
it: &it
+ edit_per_party: Modifica per partito
+ signed: Firmato
reply_to: Rispondi a
pending_by_me: In sospeso da me
add: Aggiungi
@@ -1456,6 +1496,7 @@ it: &it
hello_name: Ciao %{name}
you_are_invited_to_product_name: Sei stato invitato a %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:'
+ sent_using_product_name_in_testing_mode_html: 'Inviato utilizzando %{product_name} in Modalità di Test'
sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di %{product_name}.'
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: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia."
hi_there: Ciao
@@ -1579,7 +1620,7 @@ it: &it
schedule_account_for_deletion_: "Programmare l'eliminazione dell'account?"
account_information_has_been_updated: "Le informazioni dell'account sono state aggiornate."
should_be_a_valid_url: deve essere un URL valido
- your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 settimane. Contattaci se desideri mantenere il tuo account."
+ your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 mesi. Contattaci se desideri mantenere il tuo account."
test_mode: Modalità di test
copy: Copia
copied: Copiato
@@ -2103,6 +2144,22 @@ it: &it
tell_us_more_about_your_experience: Raccontaci di più sulla tua esperienza
extremely_dissatisfied: Estremamente insoddisfatto
extremely_satisfied: Estremamente soddisfatto
+ your_pro_plan_payment_is_overdue: Il pagamento del tuo piano Pro è in ritardo.
+ click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Fai clic qui per aggiornare i tuoi dati di pagamento e saldare la fattura per garantire un servizio ininterrotto.'
+ overdue_payment: Pagamento Scaduto
+ your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Il tuo piano Pro è stato sospeso a causa di fatture non pagate. Puoi aggiornare i tuoi dati di pagamento per saldare la fattura e continuare a utilizzare DocuSeal o annullare l'abbonamento.
+ manage_subscription: Gestisci Abbonamento
+ submission_created_by_email_html: 'Invio creato da %{email}'
+ submission_created_by_email_via_source_html: 'Invio creato da %{email} tramite %{source}'
+ submission_created_via_source_html: 'Invio creato tramite %{source}'
+ pro_user_seats_used: Posti utente Pro in uso
+ manage_plan: Gestisci piano
+ submission_sources:
+ api: API
+ bulk: Invio massivo
+ embed: Incorporamento
+ invite: Invito
+ link: Link
submission_event_names:
send_email_to_html: 'E-mail inviato a %{submitter_name}'
send_reminder_email_to_html: 'E-mail di promemoria inviato a %{submitter_name}'
@@ -2141,6 +2198,8 @@ it: &it
read: Leggi i tuoi dati
fr: &fr
+ edit_per_party: Éditer par partie
+ signed: Signé
reply_to: Répondre à
partially_completed: Partiellement complété
pending_by_me: En attente par moi
@@ -2164,6 +2223,7 @@ fr: &fr
hello_name: Bonjour %{name}
you_are_invited_to_product_name: Vous êtes invité à %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous:'
+ sent_using_product_name_in_testing_mode_html: 'Envoyé en utilisant %{product_name} en Mode Test'
sent_using_product_name_free_document_signing_html: 'Envoyé en utilisant la signature de documents gratuite de %{product_name}.'
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: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Un checksum PDF est fourni pour générer une signature de confiance.
hi_there: Bonjour
@@ -2288,7 +2348,7 @@ fr: &fr
schedule_account_for_deletion_: Programmer la suppression du compte?
account_information_has_been_updated: Les informations du compte ont été mises à jour.
should_be_a_valid_url: doit être une URL valide
- your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression du compte sera traitée dans un délai de 2 semaines. Veuillez nous contacter si vous souhaitez conserver votre compte.
+ your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression du compte sera traitée dans un délai de 2 mois. Veuillez nous contacter si vous souhaitez conserver votre compte.
test_mode: Mode test
copy: Copier
copied: Copié
@@ -2812,6 +2872,22 @@ fr: &fr
tell_us_more_about_your_experience: Parlez-nous davantage de votre expérience
extremely_dissatisfied: Extrêmement insatisfait
extremely_satisfied: Extrêmement satisfait
+ your_pro_plan_payment_is_overdue: Le paiement de votre plan Pro est en retard.
+ click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Cliquez ici pour mettre à jour vos informations de paiement et régler la facture afin de garantir un service ininterrompu.'
+ overdue_payment: Paiement En Retard
+ your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Votre plan Pro a été suspendu en raison de factures impayées. Vous pouvez mettre à jour vos informations de paiement pour régler la facture et continuer à utiliser DocuSeal ou annuler votre abonnement.
+ manage_subscription: Gérer l'Abonnement
+ submission_created_by_email_html: 'Soumission créée par %{email}'
+ submission_created_by_email_via_source_html: 'Soumission créée par %{email} via %{source}'
+ submission_created_via_source_html: 'Soumission créée via %{source}'
+ pro_user_seats_used: Places utilisateur Pro en cours d'utilisation
+ manage_plan: Gérer le plan
+ submission_sources:
+ api: API
+ bulk: Envoi en masse
+ embed: Intégration
+ invite: Invitation
+ link: Lien
submission_event_names:
send_email_to_html: 'E-mail envoyé à %{submitter_name}'
send_reminder_email_to_html: 'E-mail de rappel envoyé à %{submitter_name}'
@@ -2850,6 +2926,8 @@ fr: &fr
read: Lire vos données
pt: &pt
+ edit_per_party: Edita por festa
+ signed: Assinado
reply_to: Responder a
partially_completed: Parcialmente concluído
pending_by_me: Pendente por mim
@@ -2873,6 +2951,7 @@ pt: &pt
hello_name: Olá %{name}
you_are_invited_to_product_name: Você está convidado para %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:'
+ sent_using_product_name_in_testing_mode_html: 'Enviado usando %{product_name} no Modo de Teste'
sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de %{product_name}.'
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: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável.
hi_there: Olá
@@ -2996,7 +3075,7 @@ pt: &pt
schedule_account_for_deletion_: Agendar exclusão da conta?
account_information_has_been_updated: As informações da conta foram atualizadas.
should_be_a_valid_url: deve ser um URL válido
- your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 semanas. Entre em contato conosco se você quiser manter sua conta.
+ your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 meses. Entre em contato conosco se você quiser manter sua conta.
test_mode: Modo de teste
copy: Copiar
copied: Copiado
@@ -3520,6 +3599,22 @@ pt: &pt
tell_us_more_about_your_experience: Conte-nos mais sobre sua experiência
extremely_dissatisfied: Extremamente insatisfeito
extremely_satisfied: Extremamente satisfeito
+ your_pro_plan_payment_is_overdue: O pagamento do seu plano Pro está atrasado.
+ click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Clique aqui para atualizar seus dados de pagamento e quitar a fatura para garantir um serviço ininterrupto.'
+ overdue_payment: Pagamento Atrasado
+ your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Seu plano Pro foi suspenso devido a faturas não pagas. Você pode atualizar seus dados de pagamento para quitar a fatura e continuar usando o DocuSeal ou cancelar sua assinatura.
+ manage_subscription: Gerenciar Assinatura
+ submission_created_by_email_html: 'Envio criado por %{email}'
+ submission_created_by_email_via_source_html: 'Envio criado por %{email} via %{source}'
+ submission_created_via_source_html: 'Envio criado via %{source}'
+ pro_user_seats_used: Lugares de usuário Pro em uso
+ manage_plan: Gerenciar plano
+ submission_sources:
+ api: API
+ bulk: Envio em massa
+ embed: Incorporação
+ invite: Convite
+ link: Link
submission_event_names:
send_email_to_html: 'E-mail enviado para %{submitter_name}'
send_reminder_email_to_html: 'E-mail de lembrete enviado para %{submitter_name}'
@@ -3558,6 +3653,8 @@ pt: &pt
read: Ler seus dados
de: &de
+ edit_per_party: Bearbeiten pro Partei
+ signed: Unterschrieben
reply_to: Antworten auf
partially_completed: Teilweise abgeschlossen
pending_by_me: Ausstehend von mir
@@ -3581,6 +3678,7 @@ de: &de
hello_name: Hallo %{name}
you_are_invited_to_product_name: Sie sind eingeladen zu %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:'
+ sent_using_product_name_in_testing_mode_html: 'Gesendet über %{product_name} im Testmodus'
sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von %{product_name}.'
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üfziffer wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren.
hi_there: Hallo
@@ -3704,7 +3802,7 @@ de: &de
schedule_account_for_deletion_: Konto zur Löschung einplanen?
account_information_has_been_updated: Die Kontoinformationen wurden aktualisiert.
should_be_a_valid_url: sollte eine gültige URL sein
- your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Deine Anfrage zur Kontolöschung wird innerhalb von 2 Wochen bearbeitet. Bitte kontaktiere uns, wenn du dein Konto behalten möchtest.
+ your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Deine Anfrage zur Kontolöschung wird innerhalb von 2 Monaten bearbeitet. Bitte kontaktiere uns, wenn du dein Konto behalten möchtest.
test_mode: Testmodus
copy: Kopieren
copied: Kopiert
@@ -4228,6 +4326,22 @@ de: &de
tell_us_more_about_your_experience: Erzählen Sie uns mehr über Ihre Erfahrung
extremely_dissatisfied: Extrem unzufrieden
extremely_satisfied: Extrem zufrieden
+ your_pro_plan_payment_is_overdue: Ihre Zahlung für den Pro-Plan ist überfällig.
+ click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Klicken Sie hier, um Ihre Zahlungsdaten zu aktualisieren und die Rechnung zu begleichen, um einen unterbrechungsfreien Service sicherzustellen.'
+ overdue_payment: Überfällige Zahlung
+ your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Ihr Pro-Plan wurde aufgrund unbezahlter Rechnungen ausgesetzt. Sie können Ihre Zahlungsdaten aktualisieren, um die Rechnung zu begleichen und DocuSeal weiterhin zu nutzen, oder Ihr Abonnement kündigen.
+ manage_subscription: Abonnement Verwalten
+ submission_created_by_email_html: 'Übermittlung erstellt von %{email}'
+ submission_created_by_email_via_source_html: 'Übermittlung erstellt durch %{email} über %{source}'
+ submission_created_via_source_html: 'Übermittlung erstellt über %{source}'
+ pro_user_seats_used: Verwendete Pro-Benutzerplätze
+ manage_plan: Plan verwalten
+ submission_sources:
+ api: API
+ bulk: Massenversand
+ embed: Einbettung
+ invite: Einladung
+ link: Link
submission_event_names:
send_email_to_html: 'E-Mail gesendet an %{submitter_name}'
send_reminder_email_to_html: 'Erinnerungs-E-Mail gesendet an %{submitter_name}'
diff --git a/config/routes.rb b/config/routes.rb
index eba58222..b5cedae1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -12,6 +12,7 @@ Rails.application.routes.draw do
root 'dashboard#index'
get 'up' => 'rails/health#show'
+ get 'manifest' => 'pwa#manifest'
devise_for :users,
path: '/', only: %i[sessions passwords omniauth_callbacks],
@@ -71,6 +72,7 @@ Rails.application.routes.draw do
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
resources :submissions, only: %i[show destroy] do
resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
+ resources :events, only: %i[index], controller: 'submission_events'
end
resources :submitters, only: %i[edit update]
resources :console_redirect, only: %i[index]
@@ -79,6 +81,7 @@ Rails.application.routes.draw do
resource :testing_account, only: %i[show destroy]
resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index]
+ resources :submitters_resubmit, only: %i[update]
resources :template_folders_autocomplete, only: %i[index]
resources :webhook_secret, only: %i[show update]
resources :webhook_preferences, only: %i[update]
@@ -145,9 +148,7 @@ Rails.application.routes.draw do
get :completed
end
- resources :send_submission_email, only: %i[create] do
- get :success, on: :collection
- end
+ resources :send_submission_email, only: %i[create]
resources :submitters, only: %i[], param: 'slug' do
resources :download, only: %i[index], controller: 'submissions_download'
diff --git a/lib/abilities/template_conditions.rb b/lib/abilities/template_conditions.rb
index cadbc030..4a7ebf04 100644
--- a/lib/abilities/template_conditions.rb
+++ b/lib/abilities/template_conditions.rb
@@ -25,7 +25,7 @@ module Abilities
account_ids = [user.account_id, TemplateSharing::ALL_ID]
template.template_sharings.any? do |e|
- e.account_id.in?(account_ids) && (ability.nil? || e.ability == ability)
+ e.account_id.in?(account_ids) && (ability.nil? || e.ability == 'manage' || e.ability == ability)
end
end
end
diff --git a/lib/accounts.rb b/lib/accounts.rb
index 67427cf6..f3dd639b 100644
--- a/lib/accounts.rb
+++ b/lib/accounts.rb
@@ -153,6 +153,10 @@ module Accounts
EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY)
end
+ def can_send_invitation_emails?(_account)
+ true
+ end
+
def normalize_timezone(timezone)
tzinfo = TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone)
diff --git a/lib/docuseal.rb b/lib/docuseal.rb
index 4e08d724..f74b99d1 100644
--- a/lib/docuseal.rb
+++ b/lib/docuseal.rb
@@ -69,6 +69,10 @@ module Docuseal
@default_pkcs ||= GenerateCertificate.load_pkcs(Docuseal::CERTS)
end
+ def enable_pwa?
+ true
+ end
+
def trusted_certs
@trusted_certs ||=
ENV['TRUSTED_CERTS'].to_s.gsub('\\n', "\n").split("\n\n").map do |base64|
diff --git a/lib/number_utils.rb b/lib/number_utils.rb
index d0ab673c..4cfa3879 100644
--- a/lib/number_utils.rb
+++ b/lib/number_utils.rb
@@ -4,7 +4,16 @@ module NumberUtils
FORMAT_LOCALES = {
'dot' => 'de',
'space' => 'fr',
- 'comma' => 'en'
+ 'comma' => 'en',
+ 'usd' => 'en',
+ 'eur' => 'fr',
+ 'gbp' => 'en'
+ }.freeze
+
+ CURRENCY_SYMBOLS = {
+ 'usd' => '$',
+ 'eur' => '€',
+ 'gbp' => '£'
}.freeze
module_function
@@ -12,7 +21,9 @@ module NumberUtils
def format_number(number, format)
locale = FORMAT_LOCALES[format]
- if locale
+ if CURRENCY_SYMBOLS[format]
+ ApplicationController.helpers.number_to_currency(number, locale:, precision: 2, unit: CURRENCY_SYMBOLS[format])
+ elsif locale
ApplicationController.helpers.number_with_delimiter(number, locale:)
else
number
diff --git a/lib/pdf_icons.rb b/lib/pdf_icons.rb
index e9605f18..8f767113 100644
--- a/lib/pdf_icons.rb
+++ b/lib/pdf_icons.rb
@@ -24,6 +24,10 @@ module PdfIcons
StringIO.new(logo_new_data)
end
+ def stamp_logo_io
+ StringIO.new(stamp_logo_data)
+ end
+
def check_data
@check_data ||= PATH.join('check.png').read
end
@@ -39,4 +43,8 @@ module PdfIcons
def logo_new_data
@logo_new_data ||= PATH.join('logo_new.png').read
end
+
+ def stamp_logo_data
+ @stamp_logo_data ||= PATH.join('stamp-logo.png').read
+ end
end
diff --git a/lib/pdf_icons/stamp-logo.png b/lib/pdf_icons/stamp-logo.png
new file mode 100644
index 00000000..c5a2738c
Binary files /dev/null and b/lib/pdf_icons/stamp-logo.png differ
diff --git a/lib/send_webhook_request.rb b/lib/send_webhook_request.rb
new file mode 100644
index 00000000..d2e6ebf4
--- /dev/null
+++ b/lib/send_webhook_request.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module SendWebhookRequest
+ USER_AGENT = 'DocuSeal.com Webhook'
+
+ module_function
+
+ def call(webhook_url, event_type:, data:)
+ Faraday.post(webhook_url.url) do |req|
+ req.headers['Content-Type'] = 'application/json'
+ req.headers['User-Agent'] = USER_AGENT
+ req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present?
+
+ req.body = {
+ event_type: event_type,
+ timestamp: Time.current,
+ data: data
+ }.to_json
+
+ req.options.read_timeout = 8
+ req.options.open_timeout = 8
+ end
+ rescue Faraday::Error
+ nil
+ end
+end
diff --git a/lib/submissions.rb b/lib/submissions.rb
index 86988d09..8a89aaa6 100644
--- a/lib/submissions.rb
+++ b/lib/submissions.rb
@@ -107,20 +107,27 @@ module Submissions
def normalize_email(email)
return if email.blank?
- return email.downcase if email.to_s.include?(',')
- return email.downcase if email.to_s.include?('.gob')
- return email.downcase if email.to_s.include?('.om')
- return email.downcase if email.to_s.include?('.mm')
- return email.downcase if email.to_s.include?('.cm')
- return email.downcase if email.to_s.include?('.et')
- return email.downcase if email.to_s.include?('.mo')
- return email.downcase if email.to_s.include?('.nz')
- return email.downcase if email.to_s.include?('.za')
- return email.downcase unless email.to_s.include?('.')
+
+ return email.downcase if email.to_s.include?(',') ||
+ email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
+ email.to_s.exclude?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<'))
- Rails.logger.info("Fixed email #{email.split('@').last}") if fixed_email != email.downcase.delete_prefix('<').strip
+ return fixed_email if fixed_email == email
+
+ domain = email.to_s.split('@').last.to_s.downcase
+ fixed_domain = fixed_email.to_s.split('@').last
+
+ return email.downcase if domain == fixed_domain
+
+ if DidYouMean::Levenshtein.distance(domain, fixed_domain) > 3
+ Rails.logger.info("Skipped email fix #{domain}")
+
+ return email.downcase
+ end
+
+ Rails.logger.info("Fixed email #{domain}") if fixed_email != email.downcase.delete_prefix('<').strip
fixed_email
end
@@ -135,31 +142,57 @@ module Submissions
values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
- next unless check_document_conditions(item, values, fields_uuid_index, include_submitter_uuid:)
+ next unless check_item_conditions(item, values, fields_uuid_index, include_submitter_uuid:)
end
item
end
end
- def check_document_conditions(item, values, fields_index, include_submitter_uuid: nil)
+ def filtered_conditions_fields(submitter, only_submitter_fields: true)
+ fields = submitter.submission.template_fields || submitter.submission.template.fields
+
+ fields_uuid_index = nil
+ values = nil
+
+ fields.filter_map do |field|
+ next if field['submitter_uuid'] != submitter.uuid && only_submitter_fields
+
+ if field['conditions'].present?
+ fields_uuid_index ||= fields.index_by { |f| f['uuid'] }
+ values ||= submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
+
+ submitter_conditions = []
+
+ next unless check_item_conditions(field, values, fields_uuid_index,
+ include_submitter_uuid: submitter.uuid,
+ submitter_conditions_acc: submitter_conditions)
+
+ field = field.merge('conditions' => submitter_conditions) if submitter_conditions != field['conditions']
+ end
+
+ field
+ end
+ end
+
+ def check_item_conditions(item, values, fields_index, include_submitter_uuid: nil, submitter_conditions_acc: nil)
return true if item['conditions'].blank?
- item['conditions'].all? do |condition|
+ item['conditions'].each_with_object([]) do |condition, acc|
result =
if fields_index[condition['field_uuid']]['submitter_uuid'] == include_submitter_uuid
+ submitter_conditions_acc << condition if submitter_conditions_acc
+
true
else
Submitters::SubmitValues.check_field_condition(condition, values, fields_index)
end
- item['conditions'].each_with_object([]) do |c, acc|
- if c['operation'] == 'or'
- acc.push(acc.pop || result)
- else
- acc.push(result)
- end
- end.exclude?(false)
- end
+ if condition['operation'] == 'or'
+ acc.push(acc.pop || result)
+ else
+ acc.push(result)
+ end
+ end.exclude?(false)
end
end
diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb
index 421d431f..c2144ad8 100644
--- a/lib/submissions/create_from_submitters.rb
+++ b/lib/submissions/create_from_submitters.rb
@@ -36,8 +36,8 @@ module Submissions
is_order_sent = submitters_order == 'random' || index.zero?
- build_submitter(submission:, attrs: submitter_attrs, uuid:,
- is_order_sent:, user:,
+ build_submitter(submission:, attrs: submitter_attrs,
+ uuid:, is_order_sent:, user:, params:,
preferences: preferences.merge(submission_preferences))
end
@@ -69,6 +69,16 @@ module Submissions
end
end
+ def submitter_message_preferences(uuid, params)
+ return {} if params[:request_email_per_submitter] != '1'
+ return {} if params[:is_custom_message] != '1'
+
+ {
+ 'subject' => params.dig('submitter_preferences', uuid, 'subject'),
+ 'body' => params.dig('submitter_preferences', uuid, 'body')
+ }.compact_blank
+ end
+
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil)
template_fields = (submission.template_fields || submission.template.fields).deep_dup
@@ -175,9 +185,10 @@ module Submissions
uuid || template.submitters[index]&.dig('uuid')
end
- def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:)
+ def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:, params:)
email = Submissions.normalize_email(attrs[:email])
- submitter_preferences = Submitters.normalize_preferences(submission.account, user, attrs)
+ submitter_preferences = Submitters.normalize_preferences(submission.account, user,
+ attrs.merge(submitter_message_preferences(uuid, params)))
values = attrs[:values] || {}
phone_field_uuid = find_phone_field(submission, values)&.dig('uuid')
diff --git a/lib/submissions/filter.rb b/lib/submissions/filter.rb
index 11d40a17..27a1c375 100644
--- a/lib/submissions/filter.rb
+++ b/lib/submissions/filter.rb
@@ -37,30 +37,45 @@ module Submissions
submissions.where(created_by_user_id: user&.id || -1)
end
+ # rubocop:disable Metrics/MethodLength
def filter_by_status(submissions, filters)
- submissions = submissions.pending if filters[:status] == 'pending'
- submissions = submissions.completed if filters[:status] == 'completed'
- submissions = submissions.declined if filters[:status] == 'declined'
- submissions = submissions.expired if filters[:status] == 'expired'
-
- if filters[:status] == 'partially_completed'
- submissions =
- submissions.joins(:submitters)
- .group(:id)
- .having(Arel::Nodes::NamedFunction.new(
- 'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
- [Submitter.arel_table[:completed_at].eq(nil),
- Arel::Nodes.build_quoted(false)])]
- ).gt(0))
- .having(Arel::Nodes::NamedFunction.new(
- 'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
- [Submitter.arel_table[:completed_at].not_eq(nil),
- Arel::Nodes.build_quoted(false)])]
- ).gt(0))
+ case filters[:status]
+ when 'pending'
+ submissions.pending
+ when 'completed'
+ submissions.completed
+ when 'declined'
+ submissions.declined
+ when 'expired'
+ submissions.expired
+ when 'sent'
+ submissions.joins(:submitters)
+ .where(submitters: { opened_at: nil, completed_at: nil, declined_at: nil })
+ .where.not(submitters: { sent_at: nil })
+ .group(:id)
+ when 'opened'
+ submissions.joins(:submitters)
+ .where(submitters: { completed_at: nil, declined_at: nil })
+ .where.not(submitters: { opened_at: nil })
+ .group(:id)
+ when 'partially_completed'
+ submissions.joins(:submitters)
+ .group(:id)
+ .having(Arel::Nodes::NamedFunction.new(
+ 'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
+ [Submitter.arel_table[:completed_at].eq(nil),
+ Arel::Nodes.build_quoted(false)])]
+ ).gt(0))
+ .having(Arel::Nodes::NamedFunction.new(
+ 'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
+ [Submitter.arel_table[:completed_at].not_eq(nil),
+ Arel::Nodes.build_quoted(false)])]
+ ).gt(0))
+ else
+ submissions
end
-
- submissions
end
+ # rubocop:enable Metrics/MethodLength
def filter_by_created_at(submissions, filters)
submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present?
diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb
index 1611a68c..f787c5fb 100644
--- a/lib/submissions/generate_audit_trail.rb
+++ b/lib/submissions/generate_audit_trail.rb
@@ -175,7 +175,7 @@ module Submissions
padding: [15, 0, 8, 0],
position: :float)
- unless submission.source.in?(%w[embed api])
+ if show_verify?(submission)
column.formatted_text([{ link: verify_url, text: I18n.t('verify'), style: :link }],
font_size: 9, padding: [15, 0, 10, 0], position: :float, text_align: :right)
end
@@ -425,6 +425,10 @@ module Submissions
def maybe_add_background(_canvas, _submission, _page_size); end
+ def show_verify?(submission)
+ !submission.source.in?(%w[embed api])
+ end
+
def add_logo(column, _submission = nil)
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
diff --git a/lib/submissions/generate_combined_attachment.rb b/lib/submissions/generate_combined_attachment.rb
index e5dfc892..4e996634 100644
--- a/lib/submissions/generate_combined_attachment.rb
+++ b/lib/submissions/generate_combined_attachment.rb
@@ -23,9 +23,15 @@ module Submissions
**Submissions::GenerateResultAttachments.build_signing_params(submitter, pkcs, tsa_url)
}
- pdf.sign(io, **sign_params)
+ begin
+ pdf.sign(io, **sign_params)
+ rescue HexaPDF::MalformedPDFError => e
+ Rollbar.error(e) if defined?(Rollbar)
+
+ pdf.sign(io, write_options: { incremental: false }, **sign_params)
+ end
else
- pdf.write(io)
+ pdf.write(io, incremental: true, validate: false)
end
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb
index 1686d7ec..3039ac19 100644
--- a/lib/submissions/generate_result_attachments.rb
+++ b/lib/submissions/generate_result_attachments.rb
@@ -18,9 +18,12 @@ module Submissions
TEXT_TOP_MARGIN = 1
MAX_PAGE_ROTATE = 20
+ COURIER_FONT = 'Courier'
+
A4_SIZE = [595, 842].freeze
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
+ DEFAULT_FONTS = %w[Times Helvetica Courier].freeze
MISSING_GLYPH_REPLACE = {
'▪' => '-',
@@ -188,7 +191,9 @@ module Submissions
fill_color = field.dig('preferences', 'color').presence
- font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
+ font_name = field.dig('preferences', 'font')
+ font_name = FONT_NAME unless font_name.in?(DEFAULT_FONTS)
+ font = pdf.fonts.add(font_name)
value = submitter.values[field['uuid']]
value = field['default_value'] if field['type'] == 'heading'
@@ -435,18 +440,19 @@ module Submissions
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
- text = HexaPDF::Layout::TextFragment.create(value, font:,
- fill_color:,
- font_size:)
+ text_params = { font:, fill_color:, font_size: }
+ text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
+
+ text = HexaPDF::Layout::TextFragment.create(value, **text_params)
lines = layouter.fit([text], area['w'] * width, height).lines
box_height = lines.sum(&:height)
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
- text = HexaPDF::Layout::TextFragment.create(value,
- font:,
- fill_color:,
- font_size: (font_size / 1.4).to_i)
+ text_params[:font_size] = (font_size / 1.4).to_i
+ text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
+
+ text = HexaPDF::Layout::TextFragment.create(value, **text_params)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
@@ -454,10 +460,10 @@ module Submissions
end
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
- text = HexaPDF::Layout::TextFragment.create(value,
- font:,
- fill_color:,
- font_size: (font_size / 1.9).to_i)
+ text_params[:font_size] = (font_size / 1.9).to_i
+ text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
+
+ text = HexaPDF::Layout::TextFragment.create(value, **text_params)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
diff --git a/lib/submitters.rb b/lib/submitters.rb
index bfd168ad..bcead9f2 100644
--- a/lib/submitters.rb
+++ b/lib/submitters.rb
@@ -123,10 +123,22 @@ module Submitters
filename = ReplaceEmailVariables.call(filename_format, submitter:)
filename = filename.gsub('{document.name}', blob.filename.base)
+ filename = filename.gsub(' - {submission.status}') do
+ if submitter.submission.submitters.all?(&:completed_at?)
+ status =
+ if submitter.submission.template_fields.any? { |f| f['type'] == 'signature' }
+ I18n.t(:signed)
+ else
+ I18n.t(:completed)
+ end
+
+ " - #{status}"
+ end
+ end
filename = filename.gsub(
'{submission.completed_at}',
- I18n.l(submitter.completed_at.beginning_of_year.in_time_zone(submitter.account.timezone), format: :short)
+ I18n.l(submitter.completed_at.in_time_zone(submitter.account.timezone), format: :short)
)
"#{filename}.#{blob.filename.extension}"
diff --git a/lib/submitters/create_stamp_attachment.rb b/lib/submitters/create_stamp_attachment.rb
index f18b013a..5b5f1d64 100644
--- a/lib/submitters/create_stamp_attachment.rb
+++ b/lib/submitters/create_stamp_attachment.rb
@@ -98,7 +98,7 @@ module Submitters
end
def load_logo(_submitter)
- PdfIcons.logo_io
+ PdfIcons.stamp_logo_io
end
end
end
diff --git a/lib/submitters/maybe_assign_default_browser_signature.rb b/lib/submitters/maybe_assign_default_browser_signature.rb
index 1c0f124c..98b23131 100644
--- a/lib/submitters/maybe_assign_default_browser_signature.rb
+++ b/lib/submitters/maybe_assign_default_browser_signature.rb
@@ -18,7 +18,7 @@ module Submitters
def find_or_create_signature_from_value(submitter, value, attachments)
_, attachment = Submitters::NormalizeValues.normalize_attachment_value(value,
- 'signature',
+ { 'type' => 'signature' },
submitter.account,
attachments,
submitter)
diff --git a/lib/submitters/normalize_values.rb b/lib/submitters/normalize_values.rb
index bed672e0..82e3eed6 100644
--- a/lib/submitters/normalize_values.rb
+++ b/lib/submitters/normalize_values.rb
@@ -40,7 +40,7 @@ module Submitters
if field['type'].in?(%w[initials signature image file stamp]) && value.present?
new_value, new_attachments =
- normalize_attachment_value(value, field['type'], template.account, attachments, for_submitter)
+ normalize_attachment_value(value, field, template.account, attachments, for_submitter)
attachments.push(*new_attachments)
@@ -109,17 +109,17 @@ module Submitters
.merge(fields.index_by { |e| e['name'].to_s.downcase })
end
- def normalize_attachment_value(value, type, account, attachments, for_submitter = nil)
+ def normalize_attachment_value(value, field, account, attachments, for_submitter = nil)
if value.is_a?(Array)
new_attachments = value.map do |v|
- new_attachment = find_or_build_attachment(v, type, account, for_submitter)
+ new_attachment = find_or_build_attachment(v, field, account, for_submitter)
attachments.find { |a| a.blob_id == new_attachment.blob_id } || new_attachment
end
[new_attachments.map(&:uuid), new_attachments]
else
- new_attachment = find_or_build_attachment(value, type, account, for_submitter)
+ new_attachment = find_or_build_attachment(value, field, account, for_submitter)
existing_attachment = attachments.find { |a| a.blob_id == new_attachment.blob_id }
@@ -129,7 +129,9 @@ module Submitters
end
end
- def find_or_build_attachment(value, type, account, for_submitter = nil)
+ def find_or_build_attachment(value, field, account, for_submitter = nil)
+ type = field['type']
+
blob =
if value.match?(%r{\Ahttps?://})
find_or_create_blob_from_url(account, value)
@@ -138,6 +140,8 @@ module Submitters
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
Marcel::MimeType.for(data).exclude?('octet-stream')
find_or_create_blob_from_base64(account, data, type)
+ elsif type == 'image' && (value.starts_with?('') || value.starts_with?(' 1
+
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
- submitter.values.delete(field['uuid']) unless check_field_conditions(submitter, field, fields_uuid_index)
+ submitter_values ||= submitter.values
+
+ is_other_submitter_conditions &&= field_conditions_other_submitter?(submitter, field, fields_uuid_index)
+
+ if is_other_submitter_conditions
+ submitter_values = submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
+ end
+
+ submitter.values.delete(field['uuid']) unless check_field_conditions(submitter_values, field, fields_uuid_index)
if field['areas'].present? && field['areas'].none? { |area| attachments_index[area['attachment_uuid']] }
submitter.values.delete(field['uuid'])
@@ -199,10 +210,14 @@ module Submitters
submitter.values
end
- def check_field_conditions(submitter, field, fields_uuid_index)
- return true if field['conditions'].blank?
+ def field_conditions_other_submitter?(submitter, field, fields_uuid_index)
+ field['conditions'].to_a.any? do |c|
+ fields_uuid_index.dig(c['field_uuid'], 'submitter_uuid') != submitter.uuid
+ end
+ end
- submitter_values = submitter.values
+ def check_field_conditions(submitter_values, field, fields_uuid_index)
+ return true if field['conditions'].blank?
field['conditions'].each_with_object([]) do |c, acc|
if c['operation'] == 'or'
diff --git a/lib/templates/clone.rb b/lib/templates/clone.rb
index cd1118a3..6210354d 100644
--- a/lib/templates/clone.rb
+++ b/lib/templates/clone.rb
@@ -32,6 +32,7 @@ module Templates
template
end
+ # rubocop:disable Metrics, Style/CombinableLoops
def update_submitters_and_fields_and_schema(cloned_submitters, cloned_fields, cloned_schema)
submitter_uuids_replacements = {}
field_uuids_replacements = {}
@@ -43,6 +44,20 @@ module Templates
submitter['uuid'] = new_submitter_uuid
end
+ cloned_submitters.each do |submitter|
+ if submitter['optional_invite_by_uuid'].present?
+ submitter['optional_invite_by_uuid'] = submitter_uuids_replacements[submitter['optional_invite_by_uuid']]
+ end
+
+ if submitter['invite_by_uuid'].present?
+ submitter['invite_by_uuid'] = submitter_uuids_replacements[submitter['invite_by_uuid']]
+ end
+
+ if submitter['linked_to_uuid'].present?
+ submitter['linked_to_uuid'] = submitter_uuids_replacements[submitter['linked_to_uuid']]
+ end
+ end
+
cloned_fields.each do |field|
new_field_uuid = SecureRandom.uuid
@@ -75,5 +90,6 @@ module Templates
[cloned_submitters, cloned_fields, cloned_schema]
end
+ # rubocop:enable Metrics, Style/CombinableLoops
end
end
diff --git a/public/apple-icon-180x180.png b/public/apple-icon-180x180.png
index bc23cb2c..deb9f313 100644
Binary files a/public/apple-icon-180x180.png and b/public/apple-icon-180x180.png differ
diff --git a/public/service-worker.js b/public/service-worker.js
new file mode 100644
index 00000000..3334a5ae
--- /dev/null
+++ b/public/service-worker.js
@@ -0,0 +1,11 @@
+self.addEventListener('install', () => {
+ console.log('DocuSeal App installed')
+})
+
+self.addEventListener('activate', () => {
+ console.log('DocuSeal App activated')
+})
+
+self.addEventListener('fetch', (event) => {
+ event.respondWith(fetch(event.request))
+})
diff --git a/spec/factories/templates.rb b/spec/factories/templates.rb
index 0f737512..8f619353 100644
--- a/spec/factories/templates.rb
+++ b/spec/factories/templates.rb
@@ -9,6 +9,7 @@ FactoryBot.define do
transient do
submitter_count { 1 }
+ attachment_count { 1 }
only_field_types do
%w[text date checkbox radio signature number multiple select initials image file stamp cells phone payment]
end
@@ -16,20 +17,6 @@ FactoryBot.define do
end
after(:create) do |template, ev|
- blob = ActiveStorage::Blob.create_and_upload!(
- io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
- filename: 'sample-document.pdf',
- content_type: 'application/pdf'
- )
- attachment = ActiveStorage::Attachment.create!(
- blob:,
- name: :documents,
- record: template
- )
-
- Templates::ProcessDocument.call(attachment, attachment.download)
-
- template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
number_words = %w[first second third fourth fifth sixth seventh eighth ninth tenth]
template.submitters = Array.new(ev.submitter_count) do |i|
@@ -39,296 +26,319 @@ FactoryBot.define do
}
end
- template.fields = template.submitters.reduce([]) do |fields, submitter|
- fields += [
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'First Name',
- 'type' => 'text',
- 'required' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.09273546006944444,
- 'y' => 0.1099851117387033,
- 'w' => 0.2701497395833333,
- 'h' => 0.0372705365913556,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Birthday',
- 'type' => 'date',
- 'required' => true,
- 'preferences' => { 'format' => 'DD/MM/YYYY' },
- 'areas' => [
- {
- 'x' => 0.09166666666666666,
- 'y' => 0.1762778204144282,
- 'w' => 0.2763888888888889,
- 'h' => 0.0359029261474578,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Do you agree?',
- 'type' => 'checkbox',
- 'required' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.09051106770833334,
- 'y' => 0.227587027259332,
- 'w' => 0.2784450954861111,
- 'h' => 0.04113074042239687,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'First child',
- 'type' => 'radio',
- 'required' => true,
- 'preferences' => {},
- 'options' => [
- { 'value' => 'Girl', 'uuid' => SecureRandom.uuid },
- { 'value' => 'Boy', 'uuid' => SecureRandom.uuid }
- ],
- 'areas' => [
- {
- 'x' => 0.09027777777777778,
- 'y' => 0.3020184190330008,
- 'w' => 0.2,
- 'h' => 0.02857142857142857,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Signature',
- 'type' => 'signature',
- 'required' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.08611111111111111,
- 'y' => 0.3487183422870299,
- 'w' => 0.2,
- 'h' => 0.0707269155206287,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'House number',
- 'type' => 'number',
- 'required' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.08333333333333333,
- 'y' => 0.4582041442824252,
- 'w' => 0.2,
- 'h' => 0.02857142857142857,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Colors',
- 'type' => 'multiple',
- 'required' => true,
- 'preferences' => {},
- 'options' => [
- { 'value' => 'Red', 'uuid' => SecureRandom.uuid },
- { 'value' => 'Green', 'uuid' => SecureRandom.uuid },
- { 'value' => 'Blue', 'uuid' => SecureRandom.uuid }
- ],
- 'areas' => [
- {
- 'x' => 0.45,
- 'y' => 0.1133998465080583,
- 'w' => 0.2,
- 'h' => 0.02857142857142857,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Gender',
- 'type' => 'select',
- 'required' => true,
- 'preferences' => {},
- 'options' => [
- { 'value' => 'Male', 'uuid' => SecureRandom.uuid },
- { 'value' => 'Female', 'uuid' => SecureRandom.uuid }
- ],
- 'areas' => [
- {
- 'x' => 0.4513888888888889,
- 'y' => 0.1752954719877206,
- 'w' => 0.2,
- 'h' => 0.02857142857142857,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Initials',
- 'type' => 'initials',
- 'required' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.4486111111111111,
- 'y' => 0.2273599386032233,
- 'w' => 0.1,
- 'h' => 0.02857142857142857,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Avatar',
- 'type' => 'image',
- 'required' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.7180555555555556,
- 'y' => 0.1129547198772064,
- 'w' => 0.2,
- 'h' => 0.1414538310412574,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Attachment',
- 'type' => 'file',
- 'required' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.7166666666666667,
- 'y' => 0.3020107444359171,
- 'w' => 0.2,
- 'h' => 0.02857142857142857,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Stamp',
- 'type' => 'stamp',
- 'required' => true,
- 'readonly' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.7166666666666667,
- 'y' => 0.3771910974673829,
- 'w' => 0.2,
- 'h' => 0.0707269155206287,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Cell code',
- 'type' => 'cells',
- 'required' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.4472222222222222,
- 'y' => 0.3530851880276286,
- 'w' => 0.2,
- 'h' => 0.02857142857142857,
- 'cell_w' => 0.04,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Payment',
- 'type' => 'payment',
- 'required' => true,
- 'preferences' => { 'currency' => 'EUR', 'price' => 1000 },
- 'areas' => [
- {
- 'x' => 0.4486111111111111,
- 'y' => 0.43168073676132,
- 'w' => 0.2,
- 'h' => 0.02857142857142857,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- },
- {
- 'uuid' => SecureRandom.uuid,
- 'submitter_uuid' => submitter['uuid'],
- 'name' => 'Mobile Phone',
- 'type' => 'phone',
- 'required' => true,
- 'preferences' => {},
- 'areas' => [
- {
- 'x' => 0.44443359375,
- 'y' => 0.3010283960092095,
- 'w' => 0.2,
- 'h' => 0.02857142857142857,
- 'attachment_uuid' => attachment.uuid,
- 'page' => 0
- }
- ]
- }
- ].select { |f| ev.only_field_types.include?(f['type']) && ev.except_field_types.exclude?(f['type']) }
+ ev.attachment_count.times do |i|
+ attachment_index = i + 1 if i > 0
+ field_index = "(#{attachment_index})" if attachment_index
+
+ blob = ActiveStorage::Blob.create_and_upload!(
+ io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
+ filename: 'sample-document.pdf',
+ content_type: 'application/pdf'
+ )
+ attachment = ActiveStorage::Attachment.create!(
+ blob:,
+ name: :documents,
+ record: template
+ )
+
+ Templates::ProcessDocument.call(attachment, attachment.download)
+
+ template.schema << {
+ attachment_uuid: attachment.uuid,
+ name: ['sample-document', attachment_index].compact.join('-')
+ }
+
+ template.fields += template.submitters.reduce([]) do |fields, submitter|
+ fields += [
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['First Name', field_index].compact.join(' '),
+ 'type' => 'text',
+ 'required' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.09273546006944444,
+ 'y' => 0.1099851117387033,
+ 'w' => 0.2701497395833333,
+ 'h' => 0.0372705365913556,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Birthday', field_index].compact.join(' '),
+ 'type' => 'date',
+ 'required' => true,
+ 'preferences' => { 'format' => 'DD/MM/YYYY' },
+ 'areas' => [
+ {
+ 'x' => 0.09166666666666666,
+ 'y' => 0.1762778204144282,
+ 'w' => 0.2763888888888889,
+ 'h' => 0.0359029261474578,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Do you agree?', field_index].compact.join(' '),
+ 'type' => 'checkbox',
+ 'required' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.09051106770833334,
+ 'y' => 0.227587027259332,
+ 'w' => 0.2784450954861111,
+ 'h' => 0.04113074042239687,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['First child', field_index].compact.join(' '),
+ 'type' => 'radio',
+ 'required' => true,
+ 'preferences' => {},
+ 'options' => [
+ { 'value' => 'Girl', 'uuid' => SecureRandom.uuid },
+ { 'value' => 'Boy', 'uuid' => SecureRandom.uuid }
+ ],
+ 'areas' => [
+ {
+ 'x' => 0.09027777777777778,
+ 'y' => 0.3020184190330008,
+ 'w' => 0.2,
+ 'h' => 0.02857142857142857,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Signature', field_index].compact.join(' '),
+ 'type' => 'signature',
+ 'required' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.08611111111111111,
+ 'y' => 0.3487183422870299,
+ 'w' => 0.2,
+ 'h' => 0.0707269155206287,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['House number', field_index].compact.join(' '),
+ 'type' => 'number',
+ 'required' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.08333333333333333,
+ 'y' => 0.4582041442824252,
+ 'w' => 0.2,
+ 'h' => 0.02857142857142857,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Colors', field_index].compact.join(' '),
+ 'type' => 'multiple',
+ 'required' => true,
+ 'preferences' => {},
+ 'options' => [
+ { 'value' => 'Red', 'uuid' => SecureRandom.uuid },
+ { 'value' => 'Green', 'uuid' => SecureRandom.uuid },
+ { 'value' => 'Blue', 'uuid' => SecureRandom.uuid }
+ ],
+ 'areas' => [
+ {
+ 'x' => 0.45,
+ 'y' => 0.1133998465080583,
+ 'w' => 0.2,
+ 'h' => 0.02857142857142857,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Gender', field_index].compact.join(' '),
+ 'type' => 'select',
+ 'required' => true,
+ 'preferences' => {},
+ 'options' => [
+ { 'value' => 'Male', 'uuid' => SecureRandom.uuid },
+ { 'value' => 'Female', 'uuid' => SecureRandom.uuid }
+ ],
+ 'areas' => [
+ {
+ 'x' => 0.4513888888888889,
+ 'y' => 0.1752954719877206,
+ 'w' => 0.2,
+ 'h' => 0.02857142857142857,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Initials', field_index].compact.join(' '),
+ 'type' => 'initials',
+ 'required' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.4486111111111111,
+ 'y' => 0.2273599386032233,
+ 'w' => 0.1,
+ 'h' => 0.02857142857142857,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Avatar', field_index].compact.join(' '),
+ 'type' => 'image',
+ 'required' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.7180555555555556,
+ 'y' => 0.1129547198772064,
+ 'w' => 0.2,
+ 'h' => 0.1414538310412574,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Attachment', field_index].compact.join(' '),
+ 'type' => 'file',
+ 'required' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.7166666666666667,
+ 'y' => 0.3020107444359171,
+ 'w' => 0.2,
+ 'h' => 0.02857142857142857,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Stamp', field_index].compact.join(' '),
+ 'type' => 'stamp',
+ 'required' => true,
+ 'readonly' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.7166666666666667,
+ 'y' => 0.3771910974673829,
+ 'w' => 0.2,
+ 'h' => 0.0707269155206287,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Cell code', field_index].compact.join(' '),
+ 'type' => 'cells',
+ 'required' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.4472222222222222,
+ 'y' => 0.3530851880276286,
+ 'w' => 0.2,
+ 'h' => 0.02857142857142857,
+ 'cell_w' => 0.04,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Payment', field_index].compact.join(' '),
+ 'type' => 'payment',
+ 'required' => true,
+ 'preferences' => { 'currency' => 'EUR', 'price' => 1000 },
+ 'areas' => [
+ {
+ 'x' => 0.4486111111111111,
+ 'y' => 0.43168073676132,
+ 'w' => 0.2,
+ 'h' => 0.02857142857142857,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => SecureRandom.uuid,
+ 'submitter_uuid' => submitter['uuid'],
+ 'name' => ['Mobile Phone', field_index].compact.join(' '),
+ 'type' => 'phone',
+ 'required' => true,
+ 'preferences' => {},
+ 'areas' => [
+ {
+ 'x' => 0.44443359375,
+ 'y' => 0.3010283960092095,
+ 'w' => 0.2,
+ 'h' => 0.02857142857142857,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ }
+ ].select { |f| ev.only_field_types.include?(f['type']) && ev.except_field_types.exclude?(f['type']) }
- fields
+ fields
+ end
end
template.save!
|