Compare commits

..
20 Commits
Author SHA1 Message Date
Alex Turchyn 993698ec01 Merge from docusealco/wip 2025-08-18 12:33:13 +03:00
Pete Matsyburka fcdc44ddbe update rails 2025-08-14 09:44:04 +03:00
Pete Matsyburka 0cb6b0ae12 fix zip 2025-08-13 11:18:49 +03:00
Pete Matsyburka 0388927c5b fix readonly date 2025-08-12 22:33:38 +03:00
Pete Matsyburka 2c8f5d2129 debug file 2025-08-12 20:01:52 +03:00
Pete Matsyburka 2bffbf83c6 handle zip upload 2025-08-12 13:07:16 +03:00
Pete Matsyburka ec01529014 show readonly fields 2025-08-11 16:58:14 +03:00
Pete Matsyburka ea1abf5662 fix test email 2025-08-08 16:33:19 +03:00
Pete Matsyburka fcd5263af1 add hour minute variable 2025-08-07 11:33:11 +03:00
Pete Matsyburka 1fa8616dbc fix submitters order 2025-08-04 14:46:46 +03:00
Alex Turchyn f627277b45 Merge from docusealco/wip 2025-08-04 12:59:10 +03:00
Pete Matsyburka 809c3ea270 fix template 2fa email 2025-07-31 19:23:27 +03:00
Pete Matsyburka 9caa9d79d4 fix tooltip 2025-07-31 13:32:28 +03:00
Pete Matsyburka a8f1c1c323 add new signature formats 2025-07-31 13:13:31 +03:00
Pete Matsyburka 59fbea5c5f fix migration 2025-07-30 18:45:28 +03:00
Pete Matsyburka d3273d879a add order ui 2025-07-30 10:22:38 +03:00
Pete Matsyburka ea227e82c9 force smtp auto 2025-07-29 22:49:20 +03:00
Pete Matsyburka 676b330aa3 add submitters order param 2025-07-29 17:47:47 +03:00
Pete Matsyburka 8d3295e125 better signature validation 2025-07-29 13:51:11 +03:00
Pete Matsyburka 8cb74e0bcc fix build 2025-07-28 16:55:21 +03:00
66 changed files with 484 additions and 226 deletions

No files matched your search

+7 -2
View File
@@ -12,10 +12,15 @@ on:
type: string type: string
required: false required: false
default: tonistiigi/binfmt:latest default: tonistiigi/binfmt:latest
os:
description: OS
type: string
required: false
default: ubuntu-24.04-arm
jobs: jobs:
build: build:
runs-on: ubuntu-24.04-arm runs-on: ${{ inputs.os }}
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
@@ -29,7 +34,7 @@ jobs:
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: docuseal/docuseal images: docuseal/docuseal
tags: type=semver,pattern={{version}} tags: latest,${{ inputs.version }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
+1 -1
View File
@@ -50,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app WORKDIR /app
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge yaml-dev redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\ RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\ \n\
+86 -78
View File
@@ -1,29 +1,29 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (8.0.1) actioncable (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.0.1) actionmailbox (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activejob (= 8.0.1) activejob (= 8.0.2.1)
activerecord (= 8.0.1) activerecord (= 8.0.2.1)
activestorage (= 8.0.1) activestorage (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.0.1) actionmailer (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
actionview (= 8.0.1) actionview (= 8.0.2.1)
activejob (= 8.0.1) activejob (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.0.1) actionpack (8.0.2.1)
actionview (= 8.0.1) actionview (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
@@ -31,35 +31,35 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.0.1) actiontext (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activerecord (= 8.0.1) activerecord (= 8.0.2.1)
activestorage (= 8.0.1) activestorage (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.0.1) actionview (8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (8.0.1) activejob (8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.1) activemodel (8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
activerecord (8.0.1) activerecord (8.0.2.1)
activemodel (= 8.0.1) activemodel (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.0.1) activestorage (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activejob (= 8.0.1) activejob (= 8.0.2.1)
activerecord (= 8.0.1) activerecord (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.1) activesupport (8.0.2.1)
base64 base64
benchmark (>= 0.3) benchmark (>= 0.3)
bigdecimal bigdecimal
@@ -104,9 +104,9 @@ GEM
faraday_middleware (~> 1.0, >= 1.0.0.rc1) faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0) net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0) base64 (0.3.0)
bcrypt (3.1.20) bcrypt (3.1.20)
benchmark (0.4.0) benchmark (0.4.1)
better_html (2.1.1) better_html (2.1.1)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
@@ -114,7 +114,7 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (3.1.8) bigdecimal (3.2.2)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
@@ -141,8 +141,8 @@ GEM
cldr-plurals-runtime-rb (1.1.0) cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.3.4) concurrent-ruby (1.3.5)
connection_pool (2.4.1) connection_pool (2.5.3)
crack (1.0.0) crack (1.0.0)
bigdecimal bigdecimal
rexml rexml
@@ -174,8 +174,9 @@ GEM
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.1) docile (1.4.1)
dotenv (3.1.7) dotenv (3.1.7)
drb (2.2.1) drb (2.2.3)
email_typo (0.2.3) email_typo (0.2.3)
erb (5.0.2)
erb_lint (0.7.0) erb_lint (0.7.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
@@ -276,13 +277,14 @@ GEM
openssl (>= 2.2.1) openssl (>= 2.2.1)
htmlentities (4.3.4) htmlentities (4.3.4)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.14.6) i18n (1.14.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.13.0) image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.8.0) io-console (0.8.1)
irb (1.14.3) irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
@@ -300,13 +302,13 @@ GEM
letter_opener (~> 1.9) letter_opener (~> 1.9)
railties (>= 6.1) railties (>= 6.1)
rexml rexml
logger (1.6.4) logger (1.7.0)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.23.1) loofah (2.24.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -320,7 +322,7 @@ GEM
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.9) mini_portile2 (2.8.9)
minitest (5.25.4) minitest (5.25.5)
msgpack (1.7.5) msgpack (1.7.5)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.1) multipart-post (2.4.1)
@@ -328,14 +330,14 @@ GEM
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.5) net-http-persistent (4.0.5)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.5.6) net-imap (0.5.9)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
timeout timeout
net-smtp (0.5.0) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.9) nokogiri (1.18.9)
@@ -365,6 +367,8 @@ GEM
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.9) pg (1.5.9)
pp (0.6.2)
prettyprint
premailer (1.27.0) premailer (1.27.0)
addressable addressable
css_parser (>= 1.19.0) css_parser (>= 1.19.0)
@@ -375,42 +379,44 @@ GEM
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0) pretender (0.5.0)
actionpack (>= 6.1) actionpack (>= 6.1)
prettyprint (0.2.0)
pry (0.15.0) pry (0.15.0)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.11) pry-rails (0.3.11)
pry (>= 0.13.0) pry (>= 0.13.0)
psych (5.2.2) psych (5.2.6)
date date
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.5.0) puma (6.5.0)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.16) rack (3.2.0)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.1.0) rack-test (2.2.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (2.2.1)
rack (>= 3) rack (>= 3)
rails (8.0.1) rails (8.0.2.1)
actioncable (= 8.0.1) actioncable (= 8.0.2.1)
actionmailbox (= 8.0.1) actionmailbox (= 8.0.2.1)
actionmailer (= 8.0.1) actionmailer (= 8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
actiontext (= 8.0.1) actiontext (= 8.0.2.1)
actionview (= 8.0.1) actionview (= 8.0.2.1)
activejob (= 8.0.1) activejob (= 8.0.2.1)
activemodel (= 8.0.1) activemodel (= 8.0.2.1)
activerecord (= 8.0.1) activerecord (= 8.0.2.1)
activestorage (= 8.0.1) activestorage (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.1) railties (= 8.0.2.1)
rails-dom-testing (2.2.0) rails-dom-testing (2.3.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
@@ -424,22 +430,23 @@ GEM
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (8.0.1) railties (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.3.0)
rdoc (6.10.0) rdoc (6.14.2)
erb
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.23.0) redis-client (0.23.0)
connection_pool connection_pool
regexp_parser (2.9.3) regexp_parser (2.9.3)
reline (0.6.0) reline (0.6.2)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
@@ -538,7 +545,7 @@ GEM
sqlite3 (2.5.0-arm64-darwin) sqlite3 (2.5.0-arm64-darwin)
sqlite3 (2.5.0-x86_64-linux-gnu) sqlite3 (2.5.0-x86_64-linux-gnu)
sqlite3 (2.5.0-x86_64-linux-musl) sqlite3 (2.5.0-x86_64-linux-musl)
stringio (3.1.2) stringio (3.1.7)
strip_attributes (1.14.1) strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0) activemodel (>= 3.0, < 9.0)
thor (1.4.0) thor (1.4.0)
@@ -574,12 +581,13 @@ GEM
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1) webrick (1.9.1)
websocket-driver (0.7.6) websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.7.1) zeitwerk (2.7.3)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
+2 -2
View File
@@ -27,7 +27,7 @@ class AccountsController < ApplicationController
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS]) unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url')) @encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :show, status: :unprocessable_entity return render :show, status: :unprocessable_content
end end
@encrypted_config.save! @encrypted_config.save!
@@ -39,7 +39,7 @@ class AccountsController < ApplicationController
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated') redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
end end
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
render :show, status: :unprocessable_entity render :show, status: :unprocessable_content
end end
def destroy def destroy
+2 -2
View File
@@ -16,7 +16,7 @@ module Api
check_authorization check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e| rescue_from Params::BaseValidator::InvalidParameterError do |e|
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_content
end end
rescue_from RateLimit::LimitApproached do |e| rescue_from RateLimit::LimitApproached do |e|
@@ -33,7 +33,7 @@ module Api
rescue_from JSON::ParserError do |e| rescue_from JSON::ParserError do |e|
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_content
end end
end end
+14 -3
View File
@@ -10,10 +10,21 @@ module Api
def create def create
submitter = Submitter.find_by!(slug: params[:submitter_slug]) submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path)) if params[:type].in?(%w[initials signature])
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar) image = Vips::Image.new_from_file(params[:file].path)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity if ImageUtils.blank?(image)
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
end
if ImageUtils.error?(image)
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_content
end
end end
attachment = Submitters.create_attachment!(submitter, params) attachment = Submitters.create_attachment!(submitter, params)
@@ -50,12 +50,12 @@ module Api
def create def create
Params::SubmissionCreateValidator.call(params) Params::SubmissionCreateValidator.call(params)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil? return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
if @template.fields.blank? if @template.fields.blank?
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar) Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
end end
params[:send_email] = true unless params.key?(:send_email) params[:send_email] = true unless params.key?(:send_email)
@@ -82,7 +82,7 @@ module Api
DownloadUtils::UnableToDownload => e DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_content
end end
def destroy def destroy
@@ -185,7 +185,7 @@ module Api
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
:require_phone_2fa, :require_phone_2fa, :order,
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :required, :validation_pattern, :invalid_message, :readonly, :required, :validation_pattern, :invalid_message,
+2 -2
View File
@@ -34,7 +34,7 @@ module Api
def update def update
if @submitter.completed_at? if @submitter.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
end end
submission = @submitter.submission submission = @submitter.submission
@@ -74,7 +74,7 @@ module Api
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_content
end end
def submitter_params def submitter_params
+1 -1
View File
@@ -106,7 +106,7 @@ module Api
:external_id, :external_id,
:shared_link, :shared_link,
{ {
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]], submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :prefillable, :title, :description, :prefillable,
+3 -3
View File
@@ -7,8 +7,8 @@ module Api
def merge def merge
files = params[:files] || [] files = params[:files] || []
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank? return render json: { error: 'Files are required' }, status: :unprocessable_content if files.blank?
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2 return render json: { error: 'At least 2 files are required' }, status: :unprocessable_content if files.size < 2
render json: { render json: {
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string) data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
@@ -35,7 +35,7 @@ module Api
end end
} }
rescue HexaPDF::MalformedPDFError rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity render json: { error: 'Malformed PDF' }, status: :unprocessable_content
end end
end end
end end
@@ -15,12 +15,12 @@ class EmailSmtpSettingsController < ApplicationController
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved') redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
rescue StandardError => e rescue StandardError => e
flash[:alert] = e.message flash[:alert] = e.message
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
private private
+2 -2
View File
@@ -53,7 +53,7 @@ class EsignSettingsController < ApplicationController
@cert_record.errors.add(:name, I18n.t('already_exists')) @cert_record.errors.add(:name, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
status: :unprocessable_entity status: :unprocessable_content
end end
save_new_cert!(@encrypted_config, @cert_record) save_new_cert!(@encrypted_config, @cert_record)
@@ -64,7 +64,7 @@ class EsignSettingsController < ApplicationController
@cert_record.errors.add(:password, e.message) @cert_record.errors.add(:password, e.message)
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_content
end end
def update def update
+2 -2
View File
@@ -24,7 +24,7 @@ class MfaSetupController < ApplicationController
@error_message = I18n.t('code_is_invalid') @error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_content
end end
end end
@@ -36,7 +36,7 @@ class MfaSetupController < ApplicationController
else else
@error_message = I18n.t('code_is_invalid') @error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content
end end
end end
+2 -2
View File
@@ -11,7 +11,7 @@ class ProfileController < ApplicationController
if current_user.update(contact_params) if current_user.update(contact_params)
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update') redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
end end
@@ -20,7 +20,7 @@ class ProfileController < ApplicationController
bypass_sign_in(current_user) bypass_sign_in(current_user)
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed') redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
end end
+1 -1
View File
@@ -16,7 +16,7 @@ class SessionsController < Devise::SessionsController
end end
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank? if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content
end end
super super
+3 -3
View File
@@ -23,10 +23,10 @@ class SetupController < ApplicationController
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS]) unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url')) @encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :index, status: :unprocessable_entity return render :index, status: :unprocessable_content
end end
return render :index, status: :unprocessable_entity unless @account.valid? return render :index, status: :unprocessable_content unless @account.valid?
if @user.save if @user.save
encrypted_configs = [ encrypted_configs = [
@@ -42,7 +42,7 @@ class SetupController < ApplicationController
redirect_to newsletter_path redirect_to newsletter_path
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
end end
+4 -4
View File
@@ -42,7 +42,7 @@ class StartFormController < ApplicationController
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message @error_message = multiple_submitters_error_message
return render :show, status: :unprocessable_entity return render :show, status: :unprocessable_content
end end
if (is_new_record = @submitter.new_record?) if (is_new_record = @submitter.new_record?)
@@ -60,7 +60,7 @@ class StartFormController < ApplicationController
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
else else
render :show, status: :unprocessable_entity render :show, status: :unprocessable_content
end end
end end
end end
@@ -206,7 +206,7 @@ class StartFormController < ApplicationController
end end
def handle_require_2fa(submitter, is_new_record:) def handle_require_2fa(submitter, is_new_record:)
return render :show, status: :unprocessable_entity if submitter.errors.present? return render :show, status: :unprocessable_content if submitter.errors.present?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter) is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
@@ -223,7 +223,7 @@ class StartFormController < ApplicationController
redirect_to submit_form_path(submitter.slug) redirect_to submit_form_path(submitter.slug)
else else
render :show, status: :unprocessable_entity render :show, status: :unprocessable_content
end end
else else
Submitters.send_shared_link_email_verification_code(submitter, request:) Submitters.send_shared_link_email_verification_code(submitter, request:)
@@ -13,7 +13,7 @@ class StorageSettingsController < ApplicationController
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved') redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
end end
+1 -1
View File
@@ -66,7 +66,7 @@ class SubmissionsController < ApplicationController
rescue Submissions::CreateFromSubmitters::BaseError => e rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error', render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
locals: { error: e.message }), locals: { error: e.message }),
status: :unprocessable_entity status: :unprocessable_content
end end
def destroy def destroy
+6 -6
View File
@@ -48,20 +48,20 @@ class SubmitFormController < ApplicationController
def update def update
if @submitter.completed_at? if @submitter.completed_at?
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
end end
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end end
if @submitter.submission.expired? if @submitter.submission.expired?
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_content
end end
if @submitter.declined_at? if @submitter.declined_at?
return render json: { error: I18n.t('form_has_been_declined') }, return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity status: :unprocessable_content
end end
Submitters::SubmitValues.call(@submitter, params, request) Submitters::SubmitValues.call(@submitter, params, request)
@@ -70,9 +70,9 @@ class SubmitFormController < ApplicationController
rescue Submitters::SubmitValues::RequiredFieldError => e rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar) Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity render json: { field_uuid: e.message }, status: :unprocessable_content
rescue Submitters::SubmitValues::ValidationError => e rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_content
end end
def completed; end def completed; end
@@ -11,13 +11,13 @@ class SubmitFormDownloadController < ApplicationController
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at? return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
return head :unprocessable_entity if @submitter.declined_at? || return head :unprocessable_content if @submitter.declined_at? ||
@submitter.submission.archived_at? || @submitter.submission.archived_at? ||
@submitter.submission.expired? || @submitter.submission.expired? ||
@submitter.submission.template&.archived_at? || @submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id, AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false) value: false)
last_completed_submitter = @submitter.submission.submitters last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id) .where.not(id: @submitter.id)
@@ -7,7 +7,7 @@ class SubmitFormInviteController < ApplicationController
def create def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :unprocessable_entity unless can_invite?(submitter) return head :unprocessable_content unless can_invite?(submitter)
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid') invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid') optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
@@ -34,7 +34,7 @@ class SubmitFormInviteController < ApplicationController
head :ok head :ok
else else
head :unprocessable_entity head :unprocessable_content
end end
end end
@@ -5,7 +5,7 @@ class TemplateDocumentsController < ApplicationController
def create def create
if params[:blobs].blank? && params[:files].blank? if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
end end
old_fields_hash = @template.fields.hash old_fields_hash = @template.fields.hash
@@ -28,6 +28,6 @@ class TemplateDocumentsController < ApplicationController
) )
} }
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content
end end
end end
@@ -4,7 +4,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
def create def create
return head :unprocessable_entity if params[:files].blank? return head :unprocessable_content if params[:files].blank?
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [@template], records: [@template],
@@ -31,7 +31,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
respond_to do |f| respond_to do |f|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) } f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity } f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content }
end end
end end
end end
+2 -2
View File
@@ -78,7 +78,7 @@ class TemplatesController < ApplicationController
maybe_redirect_to_template(@template) maybe_redirect_to_template(@template)
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
end end
end end
@@ -117,7 +117,7 @@ class TemplatesController < ApplicationController
params.require(:template).permit( params.require(:template).permit(
:name, :name,
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]], { schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]], submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :prefillable, :title, :description, :prefillable,
@@ -3,6 +3,8 @@
class TemplatesDebugController < ApplicationController class TemplatesDebugController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
DEBUG_FILE = ''
def show def show
attachment = @template.documents.first attachment = @template.documents.first
@@ -16,6 +18,8 @@ class TemplatesDebugController < ApplicationController
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment])) @template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
debug_file if DEBUG_FILE.present?
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [@template], records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }] associations: [schema_documents: { preview_images_attachments: :blob }]
@@ -31,4 +35,27 @@ class TemplatesDebugController < ApplicationController
render 'templates/edit', layout: 'plain' render 'templates/edit', layout: 'plain'
end end
def debug_file
tempfile = Tempfile.new
tempfile.binmode
tempfile.write(File.read(DEBUG_FILE))
tempfile.rewind
filename = File.basename(DEBUG_FILE)
file = ActionDispatch::Http::UploadedFile.new(
tempfile:,
filename:,
type: Marcel::MimeType.for(tempfile)
)
params = { files: [file] }
documents = Templates::CreateAttachments.call(@template, params)
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
@template.update!(schema:)
end
end end
@@ -9,6 +9,10 @@ class TemplatesRecipientsController < ApplicationController
@template.submitters = @template.submitters =
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } } submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
if @template.submitters.each_with_index.all? { |s, index| s['order'] == index }
@template.submitters.each { |s| s.delete('order') }
end
@template.save! @template.save!
render json: { submitters: @template.submitters } render json: { submitters: @template.submitters }
@@ -18,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
def submitters_params def submitters_params
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
invite_by_uuid linked_to_uuid email option]] } invite_by_uuid linked_to_uuid email option order]] }
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s| params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank? next if s[:uuid].blank?
@@ -29,6 +33,7 @@ class TemplatesRecipientsController < ApplicationController
s.delete(:is_requester) s.delete(:is_requester)
end end
s[:order] = s[:order].to_i if s[:order].present?
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank? s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank? s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
+3 -3
View File
@@ -27,7 +27,7 @@ class UsersController < ApplicationController
if User.accessible_by(current_ability).exists?(email: @user.email) if User.accessible_by(current_ability).exists?(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists')) @user.errors.add(:email, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
end end
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role) @user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
@@ -37,7 +37,7 @@ class UsersController < ApplicationController
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited') redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
end end
end end
@@ -57,7 +57,7 @@ class UsersController < ApplicationController
if @user.update(attrs.except(current_user == @user ? :role : nil)) if @user.update(attrs.except(current_user == @user ? :role : nil))
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated') redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
end end
end end
+2
View File
@@ -38,6 +38,7 @@ import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group' import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container' import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor' import EmailEditor from './elements/email_editor'
import MountOnClick from './elements/mount_on_click'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -111,6 +112,7 @@ safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer) safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor) safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('mount-on-click', MountOnClick)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
+11
View File
@@ -0,0 +1,11 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', () => {
document.body.append(this.template.content)
})
}
get template () {
return document.getElementById(this.dataset.templateId)
}
}
@@ -21,7 +21,7 @@
</label> </label>
<div class="space-x-2 flex flex-none"> <div class="space-x-2 flex flex-none">
<span <span
v-if="isTextSignature && format !== 'typed' && format !== 'upload'" v-if="isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
class="tooltip" class="tooltip"
:data-tip="t('draw_signature')" :data-tip="t('draw_signature')"
> >
@@ -38,7 +38,7 @@
</a> </a>
</span> </span>
<span <span
v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'" v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
class="tooltip ml-2" class="tooltip ml-2"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }" :class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('type_text')" :data-tip="t('type_text')"
@@ -85,8 +85,8 @@
{{ t(format === 'upload' ? 'reupload' : 'redraw') }} {{ t(format === 'upload' ? 'reupload' : 'redraw') }}
</a> </a>
<span <span
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed' && format !== 'upload'" v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
class=" tooltip" class="tooltip before:translate-x-[-90%]"
:data-tip="t('drawn_signature_on_a_touchscreen_device')" :data-tip="t('drawn_signature_on_a_touchscreen_device')"
> >
<a <a
@@ -395,7 +395,7 @@ export default {
isShowQr: false, isShowQr: false,
isOtherReason: false, isOtherReason: false,
isUsePreviousValue: true, isUsePreviousValue: true,
isTextSignature: this.field.preferences?.format === 'typed', isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
uploadImageInputKey: Math.random().toString() uploadImageInputKey: Math.random().toString()
} }
}, },
+1 -1
View File
@@ -660,7 +660,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
}, },
baseUrl: { baseUrl: {
type: String, type: String,
+1 -1
View File
@@ -66,7 +66,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
}, },
withReplaceButton: { withReplaceButton: {
type: Boolean, type: Boolean,
+3 -3
View File
@@ -107,7 +107,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
} }
}, },
emits: ['success', 'error', 'loading'], emits: ['success', 'error', 'loading'],
@@ -131,7 +131,7 @@ export default {
message () { message () {
if (this.isLoading) { if (this.isLoading) {
return this.t('uploading') return this.t('uploading')
} else if (this.acceptFileTypes === 'image/*, application/pdf') { } else if (this.acceptFileTypes === 'image/*, application/pdf, application/zip') {
return this.title || this.t('add_pdf_documents_or_images') return this.title || this.t('add_pdf_documents_or_images')
} else { } else {
return this.title || this.t('add_documents_or_images') return this.title || this.t('add_documents_or_images')
@@ -146,7 +146,7 @@ export default {
methods: { methods: {
upload: Upload.methods.upload, upload: Upload.methods.upload,
onDropFiles (e) { onDropFiles (e) {
if (this.acceptFileTypes !== 'image/*, application/pdf' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) { if (this.acceptFileTypes !== 'image/*, application/pdf, application/zip' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)|(?:application\/zip)/))) {
this.$refs.input.files = e.dataTransfer.files this.$refs.input.files = e.dataTransfer.files
this.upload() this.upload()
@@ -304,7 +304,7 @@
{{ t('any') }} {{ t('any') }}
</option> </option>
<option <option
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'upload']" v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']"
:key="type" :key="type"
:value="type" :value="type"
:selected="field.preferences?.format === type" :selected="field.preferences?.format === type"
@@ -78,7 +78,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
} }
}, },
emits: ['add', 'replace', 'replace-and-clone', 'error'], emits: ['add', 'replace', 'replace-and-clone', 'error'],
+6
View File
@@ -60,6 +60,7 @@ const en = {
any: 'Any', any: 'Any',
drawn: 'Drawn', drawn: 'Drawn',
drawn_or_typed: 'Drawn or Typed', drawn_or_typed: 'Drawn or Typed',
drawn_or_upload: 'Drawn or Upload',
upload: 'Upload', upload: 'Upload',
formula: 'Formula', formula: 'Formula',
typed: 'Typed', typed: 'Typed',
@@ -322,6 +323,7 @@ const es = {
any: 'Cualquier', any: 'Cualquier',
drawn: 'Dibujado', drawn: 'Dibujado',
drawn_or_typed: 'Dibujado o Escrito', drawn_or_typed: 'Dibujado o Escrito',
drawn_or_upload: 'Dibujado o Subido',
upload: 'Subir', upload: 'Subir',
typed: 'Escrito', typed: 'Escrito',
none: 'Ninguno', none: 'Ninguno',
@@ -410,6 +412,7 @@ const it = {
any: 'Qualsiasi', any: 'Qualsiasi',
drawn: 'Disegnato', drawn: 'Disegnato',
drawn_or_typed: 'Disegnato o Digitato', drawn_or_typed: 'Disegnato o Digitato',
drawn_or_upload: 'Disegnato o Caricato',
upload: 'Caricare', upload: 'Caricare',
formula: 'Formula', formula: 'Formula',
typed: 'Digitato', typed: 'Digitato',
@@ -672,6 +675,7 @@ const pt = {
any: 'Qualquer', any: 'Qualquer',
drawn: 'Desenhado', drawn: 'Desenhado',
drawn_or_typed: 'Desenhado ou Digitado', drawn_or_typed: 'Desenhado ou Digitado',
drawn_or_upload: 'Desenhado ou Enviado',
upload: 'Carregar', upload: 'Carregar',
typed: 'Digitado', typed: 'Digitado',
none: 'Nenhum', none: 'Nenhum',
@@ -847,6 +851,7 @@ const fr = {
any: 'Tout', any: 'Tout',
drawn: 'Dessiné', drawn: 'Dessiné',
drawn_or_typed: 'Dessiné ou Tapé', drawn_or_typed: 'Dessiné ou Tapé',
drawn_or_upload: 'Dessiné ou Téléchargé',
upload: 'Téléverser', upload: 'Téléverser',
typed: 'Tapé', typed: 'Tapé',
none: 'Aucun', none: 'Aucun',
@@ -1022,6 +1027,7 @@ const de = {
any: 'Jede', any: 'Jede',
drawn: 'Gezeichnet', drawn: 'Gezeichnet',
drawn_or_typed: 'Gezeichnet oder getippt', drawn_or_typed: 'Gezeichnet oder getippt',
drawn_or_upload: 'Gezeichnet oder hochgeladen',
upload: 'Upload', upload: 'Upload',
typed: 'Getippt', typed: 'Getippt',
none: 'Keine', none: 'Keine',
+1 -1
View File
@@ -157,7 +157,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
}, },
withReplaceButton: { withReplaceButton: {
type: Boolean, type: Boolean,
+1 -1
View File
@@ -35,7 +35,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
} }
}, },
emits: ['success'], emits: ['success'],
+1 -1
View File
@@ -57,7 +57,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
} }
}, },
emits: ['success', 'error'], emits: ['success', 'error'],
+23 -9
View File
@@ -135,19 +135,33 @@ class ProcessSubmitterCompletionJob
end end
def enqueue_next_submitter_request_notification(submitter) def enqueue_next_submitter_request_notification(submitter)
next_submitter_item = submission = submitter.submission
submitter.submission.template_submitters.find do |e| submitters_index = submission.submitters.index_by(&:uuid)
sub = submitter.submission.submitters.find { |s| s.uuid == e['uuid'] }
next unless sub next_submitter_items =
if submission.template_submitters.any? { |s| s['order'] }
submitter_groups =
submission.template_submitters.group_by.with_index { |s, index| s['order'] || index }
sub.completed_at.blank? && sub.sent_at.blank? current_group_index = submitter_groups.find { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }&.first
if submitter_groups[current_group_index + 1] &&
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
.compact.all?(&:completed_at?)
submitter_groups[current_group_index + 1]
end
else
submission.template_submitters.find do |e|
sub = submitters_index[e['uuid']]
next unless sub
sub.completed_at.blank? && sub.sent_at.blank?
end
end end
return unless next_submitter_item next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] } Submitters.send_signature_requests(next_submitters)
Submitters.send_signature_requests([next_submitter])
end end
end end
+1
View File
@@ -2,6 +2,7 @@
class TemplateMailer < ApplicationMailer class TemplateMailer < ApplicationMailer
def otp_verification_email(template, email:) def otp_verification_email(template, email:)
@current_account = template.account
@template = template @template = template
@otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':')) @otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':'))
+11 -9
View File
@@ -35,17 +35,19 @@
<%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %> <%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %>
</div> </div>
</div> </div>
<div class="form-control"> <% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<%= ff.label :security_label, 'SMTP Security', class: 'label' %> <div class="form-control">
<div class="flex items-center space-x-6"> <%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %> <div class="flex items-center space-x-6">
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %> <% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %>
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %> <%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
<%= label %> <%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
<%= label %>
<% end %>
<% end %> <% end %>
<% end %> </div>
</div> </div>
</div> <% end %>
<div class="form-control"> <div class="form-control">
<%= ff.label :from_email, t('send_from_email'), class: 'label' %> <%= ff.label :from_email, t('send_from_email'), class: 'label' %>
<%= ff.email_field :from_email, value: value['from_email'], required: !Docuseal.multitenant?, class: 'base-input' %> <%= ff.email_field :from_email, value: value['from_email'], required: !Docuseal.multitenant?, class: 'base-input' %>
@@ -1,4 +1,4 @@
<% if template.preferences['submitters_order'] == 'preserved' %> <% if template.preferences['submitters_order'] == 'preserved' || template.submitters.any? { |s| s['order'] } %>
<%= f.hidden_field :preserve_order, value: '1' %> <%= f.hidden_field :preserve_order, value: '1' %>
<% elsif template.submitters.size > 1 %> <% elsif template.submitters.size > 1 %>
<div class="form-control"> <div class="form-control">
+2 -2
View File
@@ -109,7 +109,7 @@
<%= render 'submissions/annotation', annot: %> <%= render 'submissions/annotation', annot: %>
<% end %> <% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %> <% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% value ||= field['default_value'] if field['type'] == 'heading' %> <% value ||= field['default_value'] if field['type'] == 'heading' %>
<% next if value.blank? %> <% next if value.blank? %>
<% submitter = submitters_index[field['submitter_uuid']] %> <% submitter = submitters_index[field['submitter_uuid']] %>
@@ -238,7 +238,7 @@
<div class="px-1.5 mb-4"> <div class="px-1.5 mb-4">
<% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %> <% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %>
<% submitter_field_counters[field['type']] += 1 %> <% submitter_field_counters[field['type']] += 1 %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %> <% next if value.blank? %>
<% next if field['type'] == 'heading' %> <% next if field['type'] == 'heading' %>
<div class="pt-2.5 border-b border-base-300"> <div class="pt-2.5 border-b border-base-300">
+1 -1
View File
@@ -77,7 +77,7 @@
<%= render 'submit_form/annotations', annots: %> <%= render 'submit_form/annotations', annots: %>
<% end %> <% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %> <% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% value = values[field['uuid']].presence || (field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %> <% next if value.blank? %>
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %> <% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
<% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %> <% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %>
+1 -1
View File
@@ -23,7 +23,7 @@
</div> </div>
</span> </span>
</div> </div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.multitenant? %>" multiple> <input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
</div> </div>
</label> </label>
</file-dropzone> </file-dropzone>
+1 -1
View File
@@ -14,6 +14,6 @@
</span> </span>
</button> </button>
<input type="hidden" name="form_id" value="<%= form_id %>"> <input type="hidden" name="form_id" value="<%= form_id %>">
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.advanced_formats? %>" multiple> <input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>"> <input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
<% end %> <% end %>
@@ -1,4 +1,5 @@
<% close_on_submit = local_assigns.fetch(:close_on_submit, true) %> <% close_on_submit = local_assigns.fetch(:close_on_submit, true) %>
<% is_order_set = template.submitters.any? { |s| s['order'] } %>
<%= form_for template, url: template_recipients_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form }, data: { close_on_submit: } do |f| %> <%= form_for template, url: template_recipients_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form }, data: { close_on_submit: } do |f| %>
<% unless close_on_submit %> <% unless close_on_submit %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit> <toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
@@ -6,11 +7,24 @@
<div class="space-y-3 divide-y"> <div class="space-y-3 divide-y">
<% template.submitters.each_with_index do |submitter, index| %> <% template.submitters.each_with_index do |submitter, index| %>
<div class="<%= 'pt-3' if index.positive? %>"> <div class="<%= 'pt-3' if index.positive? %>">
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %> <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %>
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %> <% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
<%= ff.hidden_field :uuid %> <%= ff.hidden_field :uuid %>
<div class="form-control"> <div class="form-control">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %> <div class="flex justify-between">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<% if template.submitters.size > 2 %>
<div id="order_<%= submitter['uuid'] %>" class="mr-0.5">
<% if is_order_set %>
<%= ff.select :order, options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, submitter['order'].presence || index), {}, class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %>
<% elsif index == 0 %>
<mount-on-click data-template-id="order_fields" class="link whitespace-nowrap text-sm mt-1 mr-1 block">
<%= t('edit_order') %>
</mount-on-click>
<% end %>
</div>
<% end %>
</div>
<% if template.submitters.size == 2 %> <% if template.submitters.size == 2 %>
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %> <%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
<% else %> <% else %>
@@ -85,3 +99,16 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% if template.submitters.size > 2 && !is_order_set %>
<template id="order_fields">
<% template.submitters.each_with_index do |submitter, index| %>
<turbo-stream action="replace" target="order_<%= submitter['uuid'] %>">
<template>
<div id="order_<%= submitter['uuid'] %>">
<%= select_tag "template[submitters][#{index}][order]", options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, submitter['order'].presence || index), class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %>
</div>
</template>
</turbo-stream>
<% end %>
</template>
<% end %>
+1 -1
View File
@@ -321,7 +321,7 @@ Devise.setup do |config|
# apps is `200 OK` and `302 Found respectively`, but new apps are generated with # apps is `200 OK` and `302 Found respectively`, but new apps are generated with
# these new defaults that match Hotwire/Turbo behavior. # these new defaults that match Hotwire/Turbo behavior.
# Note: These might become the new default in future versions of Devise. # Note: These might become the new default in future versions of Devise.
config.responder.error_status = :unprocessable_entity config.responder.error_status = :unprocessable_content
config.responder.redirect_status = :see_other config.responder.redirect_status = :see_other
# ==> Configuration for :registerable # ==> Configuration for :registerable
+12
View File
@@ -24,6 +24,8 @@ en: &en
thanks: Thanks thanks: Thanks
private: Private private: Private
select: Select select: Select
party: Party
edit_order: Edit Order
invite_form_fields: Invite form fields invite_form_fields: Invite form fields
default_parties: Default parties default_parties: Default parties
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
@@ -898,6 +900,8 @@ en: &en
range_without_total: "%{from}-%{to} events" range_without_total: "%{from}-%{to} events"
es: &es es: &es
party: Parte
edit_order: Edita Pedido
select: Seleccionar select: Seleccionar
invite_form_fields: Invitar campos del formulario invite_form_fields: Invitar campos del formulario
pro: Pro pro: Pro
@@ -1776,6 +1780,8 @@ es: &es
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
it: &it it: &it
party: Parte
edit_order: Modifica Ordine
select: Seleziona select: Seleziona
invite_form_fields: Invita campi modulo invite_form_fields: Invita campi modulo
pro: Pro pro: Pro
@@ -2654,6 +2660,8 @@ it: &it
range_without_total: "%{from}-%{to} eventi" range_without_total: "%{from}-%{to} eventi"
fr: &fr fr: &fr
party: Partie
edit_order: Modifier la commande
select: Sélectionner select: Sélectionner
invite_form_fields: Inviter des champs de formulaire invite_form_fields: Inviter des champs de formulaire
pro: Pro pro: Pro
@@ -3535,6 +3543,8 @@ fr: &fr
range_without_total: "%{from} à %{to} événements" range_without_total: "%{from} à %{to} événements"
pt: &pt pt: &pt
party: Parte
edit_order: Edita Pedido
select: Selecionar select: Selecionar
invite_form_fields: Convidar campos do formulário invite_form_fields: Convidar campos do formulário
pro: Pro pro: Pro
@@ -4414,6 +4424,8 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
de: &de de: &de
party: Partei
edit_order: Bestellung bearbeiten
select: Auswählen select: Auswählen
invite_form_fields: Formularfelder einladen invite_form_fields: Formularfelder einladen
pro: Pro pro: Pro
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class PopulateSubmitterAccountId < ActiveRecord::Migration[7.1] class PopulateSubmitterAccountId < ActiveRecord::Migration[7.1]
disable_ddl_transaction disable_ddl_transaction!
class MigrationSubmitter < ApplicationRecord class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters' self.table_name = 'submitters'
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2]
disable_ddl_transaction disable_ddl_transaction!
class MigrationSubmitter < ApplicationRecord class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters' self.table_name = 'submitters'
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class PopulateWebhookUrls < ActiveRecord::Migration[7.2] class PopulateWebhookUrls < ActiveRecord::Migration[7.2]
disable_ddl_transaction disable_ddl_transaction!
class MigrationWebhookUrl < ActiveRecord::Base class MigrationWebhookUrl < ActiveRecord::Base
self.table_name = 'webhook_urls' self.table_name = 'webhook_urls'
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class AddSharedLinkToTemplates < ActiveRecord::Migration[8.0] class AddSharedLinkToTemplates < ActiveRecord::Migration[8.0]
disable_ddl_transaction disable_ddl_transaction!
class MigrationTemplate < ActiveRecord::Base class MigrationTemplate < ActiveRecord::Base
self.table_name = 'templates' self.table_name = 'templates'
@@ -1,11 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class CreateSearchEnties < ActiveRecord::Migration[8.0] class CreateSearchEnties < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def up def up
return unless adapter_name == 'PostgreSQL' return unless adapter_name == 'PostgreSQL'
enable_extension 'btree_gin'
create_table :search_entries do |t| create_table :search_entries do |t|
t.references :record, null: false, polymorphic: true, index: false t.references :record, null: false, polymorphic: true, index: false
t.bigint :account_id, null: false t.bigint :account_id, null: false
@@ -14,12 +14,24 @@ class CreateSearchEnties < ActiveRecord::Migration[8.0]
t.timestamps t.timestamps
end end
add_index :search_entries, %i[account_id tsvector], using: :gin, where: "record_type = 'Submitter'", begin
name: 'index_search_entries_on_account_id_tsvector_submitter' enable_extension 'btree_gin'
add_index :search_entries, %i[account_id tsvector], using: :gin, where: "record_type = 'Submission'", rescue StandardError
name: 'index_search_entries_on_account_id_tsvector_submission' nil
add_index :search_entries, %i[account_id tsvector], using: :gin, where: "record_type = 'Template'", end
name: 'index_search_entries_on_account_id_tsvector_template'
btree_gin_enabled = extension_enabled?('btree_gin')
add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
using: :gin, where: "record_type = 'Submitter'",
name: 'index_search_entries_on_account_id_tsvector_submitter'
add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
using: :gin, where: "record_type = 'Submission'",
name: 'index_search_entries_on_account_id_tsvector_submission'
add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
using: :gin, where: "record_type = 'Template'",
name: 'index_search_entries_on_account_id_tsvector_template'
add_index :search_entries, %i[record_id record_type], unique: true add_index :search_entries, %i[record_id record_type], unique: true
end end
@@ -28,6 +40,10 @@ class CreateSearchEnties < ActiveRecord::Migration[8.0]
drop_table :search_entries drop_table :search_entries
disable_extension 'btree_gin' begin
disable_extension 'btree_gin'
rescue StandardError
nil
end
end end
end end
@@ -4,13 +4,18 @@ class AddNgramToSearchIndex < ActiveRecord::Migration[8.0]
def change def change
return unless adapter_name == 'PostgreSQL' return unless adapter_name == 'PostgreSQL'
btree_gin_enabled = extension_enabled?('btree_gin')
add_column :search_entries, :ngram, :tsvector add_column :search_entries, :ngram, :tsvector
add_index :search_entries, %i[account_id ngram], using: :gin, where: "record_type = 'Submitter'", add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
name: 'index_search_entries_on_account_id_ngram_submitter' using: :gin, where: "record_type = 'Submitter'",
add_index :search_entries, %i[account_id ngram], using: :gin, where: "record_type = 'Submission'", name: 'index_search_entries_on_account_id_ngram_submitter'
name: 'index_search_entries_on_account_id_ngram_submission' add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
add_index :search_entries, %i[account_id ngram], using: :gin, where: "record_type = 'Template'", using: :gin, where: "record_type = 'Submission'",
name: 'index_search_entries_on_account_id_ngram_template' name: 'index_search_entries_on_account_id_ngram_submission'
add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
using: :gin, where: "record_type = 'Template'",
name: 'index_search_entries_on_account_id_ngram_template'
end end
end end
+20 -1
View File
@@ -53,8 +53,11 @@ module Accounts
ApplicationRecord.transaction do ApplicationRecord.transaction do
account.testing_accounts << testing_account account.testing_accounts << testing_account
original_email = account.users.order(:id).first.email
test_email = generate_unique_test_email(original_email)
testing_account.users.create!( testing_account.users.create!(
email: account.users.order(:id).first.email.sub('@', '+test@'), email: test_email,
first_name: 'Testing', first_name: 'Testing',
last_name: 'Environment', last_name: 'Environment',
password: SecureRandom.hex, password: SecureRandom.hex,
@@ -63,6 +66,22 @@ module Accounts
end end
end end
def generate_unique_test_email(original_email)
base_email = original_email.sub('@', '+test@')
return base_email unless User.exists?(email: base_email)
(1..3).each do |i|
test_email = original_email.sub('@', "+test#{i}@")
return test_email unless User.exists?(email: test_email)
end
timestamp = Time.current.to_i
original_email.sub('@', "+test#{timestamp}@")
end
def create_default_template(account) def create_default_template(account)
template = Template.find(1) template = Template.find(1)
+8
View File
@@ -14,4 +14,12 @@ module ImageUtils
false false
end end
def error?(image)
image = image.crop(0, 0, image.width / 4, 2)
row1, row2 = image.to_a
row1[3..] == row2[..-4] && row1.each_cons(2).none? { |a, b| a == b }
end
end end
@@ -76,6 +76,7 @@ module Params
type(submitter_params, :name, String) type(submitter_params, :name, String)
type(submitter_params, :reply_to, String) type(submitter_params, :reply_to, String)
type(submitter_params, :email, String) type(submitter_params, :email, String)
type(submitter_params, :order, Integer)
email_format(submitter_params, :email, message: 'email is invalid') email_format(submitter_params, :email, message: 'email is invalid')
email_format(submitter_params, :reply_to, message: 'reply_to email is invalid') email_format(submitter_params, :reply_to, message: 'reply_to email is invalid')
type(submitter_params, :phone, String) type(submitter_params, :phone, String)
+13 -5
View File
@@ -142,15 +142,23 @@ module Submissions
submissions.each_with_index do |submission, index| submissions.each_with_index do |submission, index|
delay_seconds = (delay + index).seconds if delay delay_seconds = (delay + index).seconds if delay
submitters = submission.submitters.reject(&:completed_at?) template_submitters = submission.template_submitters
submitters_index = submission.submitters.reject(&:completed_at?).index_by(&:uuid)
if submission.submitters_order_preserved? if template_submitters.any? { |s| s['order'] }
first_submitter = min_order = template_submitters.map.with_index { |s, i| s['order'] || i }.min
submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first
first_submitters = template_submitters.filter_map do |s|
submitters_index[s['uuid']] if s['order'] == min_order
end
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
Submitters.send_signature_requests([first_submitter], delay_seconds:) if first_submitter Submitters.send_signature_requests([first_submitter], delay_seconds:) if first_submitter
else else
Submitters.send_signature_requests(submitters, delay_seconds:) Submitters.send_signature_requests(submitters_index.values, delay_seconds:)
end end
end end
end end
+5 -2
View File
@@ -52,9 +52,12 @@ module Submissions
template_submitter = template_submitters.find { |e| e['uuid'] == uuid } template_submitter = template_submitters.find { |e| e['uuid'] == uuid }
end end
submission.template_submitters << template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid') template_submitter = template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid')
template_submitter['order'] = submitter_attrs['order'] if submitter_attrs['order'].present?
is_order_sent = submitters_order == 'random' || index.zero? submission.template_submitters << template_submitter
is_order_sent = submitters_order == 'random' || (template_submitter['order'] || index).zero?
build_submitter(submission:, attrs: submitter_attrs, build_submitter(submission:, attrs: submitter_attrs,
uuid:, is_order_sent:, user:, params:, uuid:, is_order_sent:, user:, params:,
+17 -4
View File
@@ -162,12 +162,25 @@ module Submitters
end end
def current_submitter_order?(submitter) def current_submitter_order?(submitter)
submitter_items = submitter.submission.template_submitters || submitter.submission.template.submitters submission = submitter.submission
before_items = submitter_items[0...(submitter_items.find_index { |e| e['uuid'] == submitter.uuid })] submitter_items = submission.template_submitters || submission.template.submitters
before_items.reduce(true) do |acc, item| before_items =
acc && submitter.submission.submitters.find { |e| e.uuid == item['uuid'] }&.completed_at? if submitter_items.any? { |s| s['order'] }
submitter_groups = submitter_items.group_by.with_index { |s, index| s['order'] || index }.sort_by(&:first)
current_group_index = submitter_groups.find_index { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }
submitter_groups.first(current_group_index).flat_map(&:last)
else
submitter_items.first(submitter_items.find_index { |e| e['uuid'] == submitter.uuid })
end
before_items.all? do |item|
submitter = submission.submitters.find { |e| e.uuid == item['uuid'] }
submitter.nil? || submitter.completed_at?
end end
end end
+11 -5
View File
@@ -8,6 +8,14 @@ module Submitters
VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/ VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/
NONEDITABLE_FIELD_TYPES = %w[stamp heading].freeze NONEDITABLE_FIELD_TYPES = %w[stamp heading].freeze
STRFTIME_MAP = {
'hour' => '%-k',
'minute' => '%M',
'day' => '%-d',
'month' => '%-m',
'year' => '%Y'
}.freeze
module_function module_function
def call(submitter, params, request, validate_required: true) def call(submitter, params, request, validate_required: true)
@@ -327,12 +335,10 @@ module Submitters
else else
e e
end end
when 'hour', 'minute', 'day', 'month', 'year'
with_time ? Time.current.in_time_zone(submission.account.timezone).strftime(STRFTIME_MAP[key]) : e
when 'date' when 'date'
if with_time with_time ? Time.current.in_time_zone(submission.account.timezone).to_date.to_s : e
Time.current.in_time_zone(submission.account.timezone).to_date.to_s
else
e
end
when 'role', 'email', 'phone', 'name' when 'role', 'email', 'phone', 'name'
attrs[key] || e attrs[key] || e
else else
+49 -1
View File
@@ -3,6 +3,20 @@
module Templates module Templates
module CreateAttachments module CreateAttachments
PDF_CONTENT_TYPE = 'application/pdf' PDF_CONTENT_TYPE = 'application/pdf'
ZIP_CONTENT_TYPE = 'application/zip'
X_ZIP_CONTENT_TYPE = 'application/x-zip-compressed'
JSON_CONTENT_TYPE = 'application/json'
DOCUMENT_EXTENSIONS = %w[.docx .doc .xlsx .xls .odt .rtf].freeze
DOCUMENT_CONTENT_TYPES = %w[
application/vnd.openxmlformats-officedocument.wordprocessingml.document
application/msword
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
application/vnd.ms-excel
application/vnd.oasis.opendocument.text
application/rtf
].freeze
ANNOTATIONS_SIZE_LIMIT = 6.megabytes ANNOTATIONS_SIZE_LIMIT = 6.megabytes
InvalidFileType = Class.new(StandardError) InvalidFileType = Class.new(StandardError)
PdfEncrypted = Class.new(StandardError) PdfEncrypted = Class.new(StandardError)
@@ -10,7 +24,7 @@ module Templates
module_function module_function
def call(template, params, extract_fields: false) def call(template, params, extract_fields: false)
Array.wrap(params[:files].presence || params[:file]).map do |file| extract_zip_files(params[:files].presence || params[:file]).flat_map do |file|
handle_file_types(template, file, params, extract_fields:) handle_file_types(template, file, params, extract_fields:)
end end
end end
@@ -53,6 +67,40 @@ module Templates
raise PdfEncrypted raise PdfEncrypted
end end
def extract_zip_files(files)
extracted_files = []
Array.wrap(files).each do |file|
if file.content_type == ZIP_CONTENT_TYPE || file.content_type == X_ZIP_CONTENT_TYPE
Zip::File.open(file.tempfile).each do |entry|
next if entry.directory?
tempfile = Tempfile.new(entry.name)
tempfile.binmode
entry.get_input_stream { |in_stream| IO.copy_stream(in_stream, tempfile) }
tempfile.rewind
type = Marcel::MimeType.for(tempfile, name: entry.name)
next if type.exclude?('image') &&
type != PDF_CONTENT_TYPE &&
type != JSON_CONTENT_TYPE &&
DOCUMENT_CONTENT_TYPES.exclude?(type)
extracted_files << ActionDispatch::Http::UploadedFile.new(
filename: File.basename(entry.name),
type:,
tempfile:
)
end
else
extracted_files << file
end
end
extracted_files
end
def handle_file_types(template, file, params, extract_fields:) def handle_file_types(template, file, params, extract_fields:)
if file.content_type.include?('image') || file.content_type == PDF_CONTENT_TYPE if file.content_type.include?('image') || file.content_type == PDF_CONTENT_TYPE
return handle_pdf_or_image(template, file, file.read, params, extract_fields:) return handle_pdf_or_image(template, file, file.read, params, extract_fields:)
+6 -6
View File
@@ -151,7 +151,7 @@ describe 'Submission API' do
] ]
}.to_json }.to_json
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_content)
expect(response.parsed_body).to eq({ 'error' => 'email is invalid in `submitters[0]`.' }) expect(response.parsed_body).to eq({ 'error' => 'email is invalid in `submitters[0]`.' })
end end
@@ -165,7 +165,7 @@ describe 'Submission API' do
submitters: [{ role: 'First Party', email: 'john.doe@example.com' }] submitters: [{ role: 'First Party', email: 'john.doe@example.com' }]
}.to_json }.to_json
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_content)
expect(response.parsed_body).to eq({ 'error' => 'Template does not contain fields' }) expect(response.parsed_body).to eq({ 'error' => 'Template does not contain fields' })
end end
@@ -179,7 +179,7 @@ describe 'Submission API' do
] ]
}.to_json }.to_json
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_content)
expect(response.parsed_body).to eq({ 'error' => 'role must be unique in `submitters`.' }) expect(response.parsed_body).to eq({ 'error' => 'role must be unique in `submitters`.' })
end end
@@ -193,7 +193,7 @@ describe 'Submission API' do
] ]
}.to_json }.to_json
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_content)
expect(response.parsed_body).to eq({ 'error' => 'Defined more signing parties than in template' }) expect(response.parsed_body).to eq({ 'error' => 'Defined more signing parties than in template' })
end end
@@ -209,7 +209,7 @@ describe 'Submission API' do
} }
}.to_json }.to_json
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_content)
expect(response.parsed_body).to eq({ 'error' => 'body is required in `message`.' }) expect(response.parsed_body).to eq({ 'error' => 'body is required in `message`.' })
end end
end end
@@ -235,7 +235,7 @@ describe 'Submission API' do
emails: 'amy.baker@example.com, george.morris@example.com@gmail.com' emails: 'amy.baker@example.com, george.morris@example.com@gmail.com'
}.to_json }.to_json
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_content)
expect(response.parsed_body).to eq({ 'error' => 'emails are invalid' }) expect(response.parsed_body).to eq({ 'error' => 'emails are invalid' })
end end