diff --git a/Gemfile.lock b/Gemfile.lock
index 984b23c4..bf6f2456 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -559,7 +559,7 @@ GEM
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uniform_notifier (1.16.0)
- uri (1.0.2)
+ uri (1.0.3)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue
index 0a453397..9066684e 100644
--- a/app/javascript/submission_form/area.vue
+++ b/app/javascript/submission_form/area.vue
@@ -3,7 +3,7 @@
class="field-area flex absolute lg:text-base -outline-offset-1"
dir="auto"
:style="computedStyle"
- :class="{ 'font-mono': field.preferences?.font === 'Courier', 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
+ :class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
>
+
+
+
+
+
+
-
+
-
-
- {{ formatNumber(123456789.567, format) }}
-
-
-
- {{ t('format') }}
-
-
-
+ {{ formatNumber(123456789.567, format) }}
+
+
+
-
-
- {{ method }}
-
-
-
- {{ t('method') }}
-
-
-
+
+
+
-
-
- {{ t(value) }}
-
-
-
- {{ t('align') }}
-
-
-
+ {{ method }}
+
+
+
-
-
- {{ t('none') }}
-
-
- {{ option.value || `${t('option')} ${index + 1}` }}
-
-
-
- {{ t('default_value') }}
-
-
-
+
+
+
-
-
- {{ t('default_value') }}
-
-
-
+ {{ t(value) }}
+
+
+
-
-
- {{ t('none') }}
-
-
- {{ t(key) }}
-
-
- {{ t('custom') }}
-
-
-
- {{ t('validation') }}
-
-
-
+
+
+
-
-
- {{ t('regexp_validation') }}
-
-
-
+ {{ t('none') }}
+
+
+ {{ option.value || `${t('option')} ${index + 1}` }}
+
+
+
-
-
- {{ formatDate(new Date(), format) }}
-
-
-
- {{ t('format') }}
-
-
-
+
+
+
-
-
- {{ t('any') }}
-
-
- {{ t(type) }}
-
-
-
- {{ t('format') }}
-
-
-
-
-
- {{ t('required') }}
-
-
-
+
+
+
-
-
- {{ t('with_logo') }}
-
-
-
+ {{ t('none') }}
+
+
+ {{ t(key) }}
+
+
+ {{ t('custom') }}
+
+
+
-
-
- {{ t('only_with_camera') }}
-
-
-
+
+
+
-
-
- {{ t('checked') }}
-
-
-
-
-
- {{ t('set_signing_date') }}
-
-
-
+
+
+
-
-
- {{ t('read_only') }}
-
-
-
+ {{ formatDate(new Date(), format) }}
+
+
+
-
+
+
+
+
+ {{ t('any') }}
+
+
+ {{ t(type) }}
+
+
+
+ {{ t('format') }}
+
+
+
+
+
+ {{ t('required') }}
+
+
+
+
+
+ {{ t('with_logo') }}
+
+
+
+
+
+ {{ t('only_with_camera') }}
+
+
+
+
+
+ {{ t('checked') }}
+
+
+
+
+
+ {{ t('set_signing_date') }}
+
+
+
+
+
+ {{ t('read_only') }}
+
+
+
+
+
-
-
-
+
+
+ {{ t('font') }}
+
+
+
+
+
+
+
{{ t('description') }}
-
-
-
+
+
+
-
-
-
+
+
{{ t('condition') }}
-
-
-
-
-
-
+
+
+
+
+
+
{{ t('formula') }}
-
+
+
+
+
+
+
+
+ {{ t('page') }}
+ {{ template.schema.findIndex((item) => item.attachment_uuid === area.attachment_uuid) + 1 }}- {{ area.page + 1 }}
+
+
-
-
-
-
-
- {{ t('page') }}
-
- {{ template.schema.findIndex((item) => item.attachment_uuid === area.attachment_uuid) + 1 }}-
-
- {{ area.page + 1 }}
-
-
-
-
-
-
- {{ t('draw_new_area') }}
-
-
-
-
-
-
- {{ t('copy_to_all_pages') }}
-
+
+
+
+ {{ t('draw_new_area') }}
+
+
+
+
+
+ {{ t('copy_to_all_pages') }}
+
+
\ No newline at end of file
diff --git a/app/javascript/template_builder/font_modal.vue b/app/javascript/template_builder/font_modal.vue
new file mode 100644
index 00000000..7b2dbac4
--- /dev/null
+++ b/app/javascript/template_builder/font_modal.vue
@@ -0,0 +1,290 @@
+
+
+
+
+
+
+ {{ t('font') }} - {{ field.name || buildDefaultName(field, template.fields) }}
+
+
×
+
+
+
+
+
+
+
+
+ {{ preferences.font || 'Default' }}
+
+
+
+
+
+
+
+
+
+ Auto
+
+
+ {{ size }}
+
+
+
+ pt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ color.label }}
+
+
+
+
+
+
+
+
+ {{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
+
+
+
+
+
+ {{ t('save') }}
+
+
+
+
+
+
+
+
diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js
index 62166bc7..390ba550 100644
--- a/app/javascript/template_builder/i18n.js
+++ b/app/javascript/template_builder/i18n.js
@@ -1,4 +1,5 @@
const en = {
+ font: 'Font',
party: 'Party',
method: 'Method',
reorder_fields: 'Reorder fields',
@@ -162,6 +163,7 @@ const en = {
}
const es = {
+ fuente: 'Fuente',
party: 'Parte',
method: 'Método',
reorder_fields: 'Reordenar campos',
@@ -325,6 +327,7 @@ const es = {
}
const it = {
+ font: 'Carattere',
party: 'Parte',
method: 'Metodo',
reorder_fields: 'Riordina i campi',
@@ -488,6 +491,7 @@ const it = {
}
const pt = {
+ fonte: 'Fonte',
party: 'Parte',
method: 'Método',
reorder_fields: 'Reorganizar campos',
@@ -651,6 +655,7 @@ const pt = {
}
const fr = {
+ font: 'Police',
party: 'Partie',
method: 'Méthode',
reorder_fields: 'Réorganiser les champs',
@@ -814,6 +819,7 @@ const fr = {
}
const de = {
+ font: 'Schriftart',
party: 'Partei',
method: 'Verfahren',
reorder_fields: 'Felder neu anordnen',
diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb
index d0f63ce7..b8e945d5 100644
--- a/app/views/accounts/show.html.erb
+++ b/app/views/accounts/show.html.erb
@@ -63,7 +63,7 @@
<%= t('add_signature_id_to_the_documents') %>
- <%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
+ <%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
<% end %>
<% end %>
@@ -75,7 +75,7 @@
width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
+<% font_type = field.dig('preferences', 'font_type') %>
+width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %>
diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb
index 3256ad36..b027c0c8 100644
--- a/app/views/submissions/show.html.erb
+++ b/app/views/submissions/show.html.erb
@@ -19,7 +19,7 @@
<%= svg_icon('external_link', class: 'w-6 h-6') %>
<%= t('audit_log') %>
- <% else %>
+ <% elsif signed_in? %>
<%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %>
<%= svg_icon('logs', class: 'w-6 h-6') %>
<%= t('event_log') %>
@@ -105,7 +105,18 @@
<% value = values[field['uuid']] %>
<% value ||= field['default_value'] if field['type'] == 'heading' %>
<% next if value.blank? %>
- <%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
+ <% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
+
+
+ <%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
+
+
+ <%= render 'submissions/value', area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
+
+
+ <% else %>
+ <%= render 'submissions/value', area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
+ <% end %>
<% end %>
@@ -232,12 +243,21 @@
<% elsif field['type'] == 'checkbox' %>
<%= svg_icon('check', class: 'w-6 h-6') %>
- <% elsif field['type'] == 'number' %>
- <%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
- <% elsif field['type'] == 'date' %>
- <%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
<% else %>
- <%= Array.wrap(value).join(', ') %>
+ <% if field['type'] == 'number' %>
+ <% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
+ <% elsif field['type'] == 'date' %>
+ <% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
+ <% end %>
+ <% if (mask = field.dig('preferences', 'mask').presence) %>
+ <% if signed_in? && can?(:read, @submission) %>
+ <%= Array.wrap(value).join(', ') %>
+ <% else %>
+ <%= Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') %>
+ <% end %>
+ <% else %>
+ <%= Array.wrap(value).join(', ') %>
+ <% end %>
<% end %>
diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb
index 02967492..b4774423 100644
--- a/app/views/submit_form/show.html.erb
+++ b/app/views/submit_form/show.html.erb
@@ -58,7 +58,7 @@
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
- <%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
+ <%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
<% end %>
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 147e5113..154deb25 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -187,7 +187,7 @@ Devise.setup do |config|
# ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again.
- config.remember_for = 2.years
+ config.remember_for = ENV.fetch('SESSION_REMEMBER_DAYS', '730').to_i.days
# Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true
diff --git a/config/puma.rb b/config/puma.rb
index 964a3298..ed99f0c9 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -33,8 +33,14 @@ environment ENV.fetch('RAILS_ENV', 'development')
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
-#
-workers ENV.fetch('WEB_CONCURRENCY', 0)
+
+if ENV['WEB_CONCURRENCY_AUTO'] == 'true'
+ require 'etc'
+
+ workers Etc.nprocessors
+else
+ workers ENV.fetch('WEB_CONCURRENCY', 0)
+end
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
diff --git a/config/routes.rb b/config/routes.rb
index b5cedae1..57e02300 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -122,6 +122,8 @@ Rails.application.routes.draw do
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service
put '/disk/:encoded_token' => 'active_storage/disk#update', as: :update_rails_disk_service
post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads
+
+ ActiveSupport.run_load_hooks(:multitenant_routes, self)
end
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb
index f787c5fb..3bc461a6 100644
--- a/lib/submissions/generate_audit_trail.rb
+++ b/lib/submissions/generate_audit_trail.rb
@@ -359,13 +359,14 @@ module Submissions
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
end
- if field['type'] == 'number'
- value = NumberUtils.format_number(value,
- field.dig('preferences', 'format'))
- end
+ value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
value = value.join(', ') if value.is_a?(Array)
+ if (mask = field.dig('preferences', 'mask').presence)
+ value = TextUtils.mask_value(value, mask)
+ end
+
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
padding: [0, 0, 10, 0])
diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb
index 3039ac19..2fe766c0 100644
--- a/lib/submissions/generate_result_attachments.rb
+++ b/lib/submissions/generate_result_attachments.rb
@@ -4,12 +4,29 @@ module Submissions
module GenerateResultAttachments
FONT_SIZE = 11
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
+ FONT_BOLD_PATH = '/fonts/GoNotoKurrent-Bold.ttf'
FONT_NAME = if File.exist?(FONT_PATH)
FONT_PATH
else
'Helvetica'
end
+ FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH)
+ FONT_BOLD_PATH
+ else
+ 'Helvetica'
+ end
+
+ FONT_ITALIC_NAME = 'Helvetica'
+ FONT_BOLD_ITALIC_NAME = 'Helvetica'
+
+ FONT_VARIANS = {
+ none: FONT_NAME,
+ bold: FONT_BOLD_NAME,
+ italic: FONT_ITALIC_NAME,
+ bold_italic: FONT_BOLD_ITALIC_NAME
+ }.freeze
+
SIGN_REASON = 'Signed by %s with DocuSeal.com'
RTL_REGEXP = TextUtils::RTL_REGEXP
@@ -18,12 +35,15 @@ 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
+ FONTS_LINE_HEIGHT = {
+ 'Times' => 1.4,
+ 'Helvetica' => 1.4,
+ 'Courier' => 1.6
+ }.freeze
MISSING_GLYPH_REPLACE = {
'▪' => '-',
@@ -192,8 +212,16 @@ module Submissions
fill_color = field.dig('preferences', 'color').presence
font_name = field.dig('preferences', 'font')
+ font_variant = (field.dig('preferences', 'font_type').presence || 'none').to_sym
+
font_name = FONT_NAME unless font_name.in?(DEFAULT_FONTS)
- font = pdf.fonts.add(font_name)
+
+ if font_variant != :none && font_name == FONT_NAME
+ font_name = FONT_VARIANS[font_variant]
+ font_variant = nil unless font_name.in?(DEFAULT_FONTS)
+ end
+
+ font = pdf.fonts.add(font_name, variant: font_variant)
value = submitter.values[field['uuid']]
value = field['default_value'] if field['type'] == 'heading'
@@ -391,6 +419,10 @@ module Submissions
when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? }
cell_width = area['cell_w'] * width
+ if (mask = field.dig('preferences', 'mask').presence)
+ value = TextUtils.mask_value(value, mask)
+ end
+
chars = TextUtils.maybe_rtl_reverse(value).chars
chars = chars.reverse if field.dig('preferences', 'align') == 'right'
@@ -440,8 +472,12 @@ module Submissions
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
+ if (mask = field.dig('preferences', 'mask').presence)
+ value = TextUtils.mask_value(value, mask)
+ end
+
text_params = { font:, fill_color:, font_size: }
- text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
+ text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
@@ -450,7 +486,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
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_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
@@ -461,7 +497,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
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_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
diff --git a/lib/text_utils.rb b/lib/text_utils.rb
index 8d33f3ef..f286311f 100644
--- a/lib/text_utils.rb
+++ b/lib/text_utils.rb
@@ -2,6 +2,8 @@
module TextUtils
RTL_REGEXP = /[\p{Hebrew}\p{Arabic}]/
+ MASK_REGEXP = /[^\s\-_\[\]\(\)\+\?\.\,]/
+ MASK_SYMBOL = 'X'
module_function
@@ -13,6 +15,24 @@ module TextUtils
false
end
+ def mask_value(text, unmask_size = 0)
+ if unmask_size.is_a?(Numeric) && !unmask_size.zero? && unmask_size.abs < text.length
+ if unmask_size.negative?
+ [
+ text.first(text.length + unmask_size).gsub(MASK_REGEXP, MASK_SYMBOL),
+ text.last(-unmask_size)
+ ].join
+ elsif unmask_size.positive?
+ [
+ text.first(unmask_size),
+ text.last(text.length - unmask_size).gsub(MASK_REGEXP, MASK_SYMBOL)
+ ].join
+ end
+ else
+ text.to_s.gsub(MASK_REGEXP, MASK_SYMBOL)
+ end
+ end
+
def maybe_rtl_reverse(text)
if text.match?(RTL_REGEXP)
TwitterCldr::Shared::Bidi
diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb
index c6e512fe..be486c65 100644
--- a/spec/system/signing_form_spec.rb
+++ b/spec/system/signing_form_spec.rb
@@ -772,6 +772,30 @@ RSpec.describe 'Signing Form', type: :system do
end
end
+ context 'when the masked field' do
+ let(:template) { create(:template, submitter_count: 2, account:, author:, only_field_types: %w[text]) }
+ let(:submission) { create(:submission, template: template) }
+ let!(:first_submitter) { create(:submitter, submission:, uuid: template.submitters[0]['uuid'], account:) }
+ let!(:second_submitter) { create(:submitter, submission:, uuid: template.submitters[1]['uuid'], account:) }
+
+ it 'shows the masked value instead of the real value' do
+ field = submission.template_fields.find do |f|
+ f['name'] == 'First Name' && f['submitter_uuid'] == first_submitter.uuid
+ end
+ field['preferences']['mask'] = true
+ submission.save!
+
+ visit submit_form_path(slug: first_submitter.slug)
+
+ fill_in 'First Name', with: 'Jahn'
+ click_button 'Complete'
+
+ visit submit_form_path(slug: second_submitter.slug)
+
+ expect(page).to have_content('XXXX')
+ end
+ end
+
it 'sends completed email' do
template = create(:template, account:, author:, only_field_types: %w[text signature])
submission = create(:submission, template:)