mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 22:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eb4e8c1f8 | ||
|
|
984df15949 | ||
|
|
aa3bd24332 | ||
|
|
250889623d | ||
|
|
862fa55986 | ||
|
|
0f2889904a | ||
|
|
49f14c7929 | ||
|
|
c3f3c5d5df | ||
|
|
274259f305 | ||
|
|
a15d7a9cf3 | ||
|
|
7d154fc28d | ||
|
|
6727d3be83 | ||
|
|
4bbd5c3bcb | ||
|
|
5995f2e427 | ||
|
|
0910c8e100 | ||
|
|
8df76dfa90 | ||
|
|
78e9e66cb0 | ||
|
|
71c61fc4b3 | ||
|
|
a728aa8689 | ||
|
|
5a3174a720 | ||
|
|
652072d660 | ||
|
|
323f5c0c15 | ||
|
|
229d78c78b | ||
|
|
9d9a955149 | ||
|
|
b40a047190 | ||
|
|
ff9e415823 | ||
|
|
2f7a4fb2b3 | ||
|
|
f8a654a5b6 | ||
|
|
4449475467 | ||
|
|
bcbbf0dd42 | ||
|
|
19b9ba52d4 | ||
|
|
bdc30db2dc | ||
|
|
74634ba96b | ||
|
|
77feb05bad | ||
|
|
a1f05d006e | ||
|
|
cc3dae1335 | ||
|
|
d9debcfb0d | ||
|
|
8770d16d52 | ||
|
|
84953481e4 | ||
|
|
df7aa59f6f | ||
|
|
008b83119d | ||
|
|
15dafcb6ff | ||
|
|
7d291ed888 | ||
|
|
a7221f0750 | ||
|
|
9a5b2a5d30 | ||
|
|
7361a00d42 | ||
|
|
e07d402782 | ||
|
|
a34354bee1 | ||
|
|
84b762b529 | ||
|
|
2a839951b4 | ||
|
|
26d7c2f047 | ||
|
|
151a5b85f3 | ||
|
|
3f506f53b4 |
No files matched your search
+2
-4
@@ -2,9 +2,7 @@ FROM ruby:3.2.2-alpine3.18 as fonts
|
|||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
RUN apk --no-cache add wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
|
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine3.18 as webpack
|
FROM ruby:3.2.2-alpine3.18 as webpack
|
||||||
|
|
||||||
@@ -55,7 +53,7 @@ COPY ./public ./public
|
|||||||
COPY ./tmp ./tmp
|
COPY ./tmp ./tmp
|
||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
|
||||||
COPY --from=fonts /fonts/LiberationSans-Regular.ttf /fonts/LiberationSans-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts
|
||||||
|
|||||||
+1
-1
@@ -261,7 +261,7 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.1.0)
|
hashdiff (1.1.0)
|
||||||
hashery (2.1.2)
|
hashery (2.1.2)
|
||||||
hexapdf (0.34.1)
|
hexapdf (0.37.1)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
|
|||||||
@@ -33,17 +33,29 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
[](https://demo.docuseal.co)
|
[](https://demo.docuseal.co)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] PDF form fields builder (WYSIWYG)
|
- PDF form fields builder (WYSIWYG)
|
||||||
- [x] 11 field types available (Signature, Date, File, Checkbox etc.)
|
- 12 field types available (Signature, Date, File, Checkbox etc.)
|
||||||
- [x] Multiple submitters per document
|
- Multiple submitters per document
|
||||||
- [x] Automated emails via SMTP
|
- Automated emails via SMTP
|
||||||
- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||||
- [x] Automatic PDF eSignature
|
- Automatic PDF eSignature
|
||||||
- [x] PDF signature verification
|
- PDF signature verification
|
||||||
- [x] Users management
|
- Users management
|
||||||
- [x] Mobile-optimized
|
- Mobile-optimized
|
||||||
- [x] API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- [x] Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
|
## Pro Features
|
||||||
|
- Company logo and white-label
|
||||||
|
- User roles
|
||||||
|
- Automated reminders
|
||||||
|
- Invitation and identify verification via SMS
|
||||||
|
- SSO / SAML
|
||||||
|
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
||||||
|
- Template creation with PDF or DOCX and text tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||||
|
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- [Learn more](https://www.docuseal.co/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@@ -77,7 +89,7 @@ sudo HOST=your-domain-name.com docker compose up
|
|||||||
```
|
```
|
||||||
|
|
||||||
## For Businesses
|
## For Businesses
|
||||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
### Integrate seamless document signing into your web or mobile apps with DocuSeal
|
||||||
|
|
||||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||||
|
|
||||||
|
**We have a bug bounty program to reward security researchers.**
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class ActiveStorageBlobsProxyController < ApiBaseController
|
||||||
|
include ActiveStorage::Streaming
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :set_cors_headers
|
||||||
|
|
||||||
|
def show
|
||||||
|
blob_uuid, = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
|
|
||||||
|
if blob_uuid.blank?
|
||||||
|
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
|
if request.headers['Range'].present?
|
||||||
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class ActiveStorageBlobsProxyLegacyController < ApiBaseController
|
||||||
|
include ActiveStorage::Streaming
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def show
|
||||||
|
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.find_signed!(params[:signed_blob_id] || params[:signed_id])
|
||||||
|
|
||||||
|
is_permitted = blob.attachments.any? do |a|
|
||||||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
|
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||||
|
a.name == 'logo'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless is_permitted
|
||||||
|
Rollbar.error("Blob account not found: #{blob.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Not authenticated' }, status: :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
if request.headers['Range'].present?
|
||||||
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -21,13 +21,13 @@ module Api
|
|||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: e.message }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from JSON::ParserError do |e|
|
rescue_from JSON::ParserError do |e|
|
||||||
Rollbar.error(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_entity
|
||||||
end
|
end
|
||||||
@@ -59,5 +59,13 @@ module Api
|
|||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_cors_headers
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
headers['Access-Control-Allow-Headers'] = '*'
|
||||||
|
headers['Access-Control-Max-Age'] = '1728000'
|
||||||
|
headers['Access-Control-Allow-Credentials'] = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -74,7 +74,7 @@ module Api
|
|||||||
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
||||||
|
|
||||||
if @template.fields.blank?
|
if @template.fields.blank?
|
||||||
Rollbar.error("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_entity
|
||||||
end
|
end
|
||||||
@@ -93,7 +93,9 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
render json: submissions.flat_map(&:submitters)
|
render json: submissions.flat_map(&:submitters)
|
||||||
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
rescue Submitters::NormalizeValues::BaseError => e
|
||||||
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
submitter.opened_at = Time.current
|
||||||
|
submitter.save
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||||
|
|
||||||
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module Api
|
|||||||
render json: {
|
render json: {
|
||||||
schema:,
|
schema:,
|
||||||
documents: documents.as_json(
|
documents: documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: {
|
include: {
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
redirect_to root_path, alert: e.message
|
redirect_to root_path, alert: e.message
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EncryptedUserConfigsController < ApplicationController
|
||||||
|
load_and_authorize_resource :encrypted_user_config
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@encrypted_user_config.destroy!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,9 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
FORMAT = Templates::ProcessDocument::FORMAT
|
FORMAT = Templates::ProcessDocument::FORMAT
|
||||||
|
|
||||||
def show
|
def show
|
||||||
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
||||||
|
|
||||||
|
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid, name: :preview_images) if attachment_uuid
|
||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
@@ -21,9 +23,10 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
find_or_create_document_tempfile_path(attachment)
|
find_or_create_document_tempfile_path(attachment)
|
||||||
end
|
end
|
||||||
|
|
||||||
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
preview_image =
|
||||||
|
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||||
|
|
||||||
render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
|
redirect_to preview_image.url, allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_document_tempfile_path(attachment)
|
def find_or_create_document_tempfile_path(attachment)
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ class SessionsController < Devise::SessionsController
|
|||||||
before_action :configure_permitted_parameters
|
before_action :configure_permitted_parameters
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
|
email = sign_in_params[:email].to_s.downcase
|
||||||
|
|
||||||
|
if Docuseal.multitenant? && !User.exists?(email:)
|
||||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||||
notice: 'Create a new account'
|
notice: 'Create a new account'
|
||||||
end
|
end
|
||||||
|
|
||||||
if User.exists?(email: sign_in_params[: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_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||||
|
|
||||||
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 20.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@@ -13,9 +15,18 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter.completed_at?
|
||||||
|
|
||||||
|
if last_submitter.completed_at < TTL.ago &&
|
||||||
|
(current_user.nil? || !current_user.account.submitters.exists?(id: last_submitter.id))
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
urls =
|
urls =
|
||||||
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
||||||
helpers.rails_blob_url(attachment)
|
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: urls
|
render json: urls
|
||||||
|
|||||||
@@ -6,9 +6,21 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
|
TTL = 20.minutes
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submission.find_by!(slug: params[:slug])
|
@submission = Submission.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
||||||
|
raise ActionController::RoutingError, 'Not Found'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless submission_valid_ttl?(@submission)
|
||||||
|
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||||
|
end
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@submission],
|
records: [@submission],
|
||||||
associations: [:template, { template_schema_documents: :blob }]
|
associations: [:template, { template_schema_documents: :blob }]
|
||||||
@@ -26,4 +38,20 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
render 'submissions/show', layout: 'plain'
|
render 'submissions/show', layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def completed
|
||||||
|
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
render :completed, layout: 'plain'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submission_valid_ttl?(submission)
|
||||||
|
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
||||||
|
|
||||||
|
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||||
|
|
||||||
|
last_submitter && last_submitter.completed_at > TTL.ago
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -33,7 +33,7 @@ class TemplatesController < ApplicationController
|
|||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
@@ -51,12 +51,17 @@ class TemplatesController < ApplicationController
|
|||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
end
|
end
|
||||||
|
|
||||||
@template.account = current_account
|
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
||||||
|
@template.account_id = params[:account_id]
|
||||||
|
@template.folder = @template.account.default_template_folder
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
end
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
redirect_to edit_template_path(@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_entity
|
||||||
end
|
end
|
||||||
@@ -81,6 +86,18 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def authorized_clone_account_id?(account_id)
|
||||||
|
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_to_template(template)
|
||||||
|
if template.account == current_account
|
||||||
|
redirect_to(edit_template_path(@template))
|
||||||
|
else
|
||||||
|
redirect_back(fallback_location: root_path, notice: 'Template has been clonned')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name)
|
params.require(:template).permit(:name)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TemplatesPreviewController < ApplicationController
|
|||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
id="minimize_form_button"
|
id="minimize_form_button"
|
||||||
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
@click.prevent="isFormVisible = false"
|
@click.prevent="isFormVisible = false"
|
||||||
>
|
>
|
||||||
<IconArrowsDiagonalMinimize2
|
<IconArrowsDiagonalMinimize2
|
||||||
@@ -40,13 +40,16 @@
|
|||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div
|
||||||
|
:class="{ 'md:px-4': isBreakpointMd }"
|
||||||
|
>
|
||||||
<form
|
<form
|
||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
ref="form"
|
ref="form"
|
||||||
:action="submitPath"
|
:action="submitPath"
|
||||||
method="post"
|
method="post"
|
||||||
:class="{ 'md:mx-16': isBreakpointMd }"
|
class="mx-auto"
|
||||||
|
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
|
||||||
@submit.prevent="submitStep"
|
@submit.prevent="submitStep"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -254,7 +257,7 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:previous-value="previousSignatureValue"
|
:previous-value="previousSignatureValueFor(currentField)"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:with-typed-signature="withTypedSignature"
|
:with-typed-signature="withTypedSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
@@ -518,6 +521,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
i18n: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
withSendCopyButton: {
|
withSendCopyButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -566,11 +574,6 @@ export default {
|
|||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
previousSignatureValue () {
|
|
||||||
const signatureField = [...this.fields].reverse().find((field) => field.type === 'signature' && !!this.values[field.uuid])
|
|
||||||
|
|
||||||
return this.values[signatureField?.uuid]
|
|
||||||
},
|
|
||||||
previousInitialsValue () {
|
previousInitialsValue () {
|
||||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
|
||||||
@@ -675,7 +678,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
t (key) {
|
t (key) {
|
||||||
return i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
|
return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
|
||||||
},
|
},
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const { queryParams } = this
|
const { queryParams } = this
|
||||||
@@ -736,6 +739,13 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
previousSignatureValueFor (field) {
|
||||||
|
const signatureField = [...this.fields].reverse().find((f) =>
|
||||||
|
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
||||||
|
)
|
||||||
|
|
||||||
|
return this.values[signatureField?.uuid]
|
||||||
|
},
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
this.showFillAllRequiredFields = false
|
this.showFillAllRequiredFields = false
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const en = {
|
|||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cells',
|
cells: 'Cells',
|
||||||
stamp: 'Stamp',
|
stamp: 'Stamp',
|
||||||
|
minimize: 'Minimize',
|
||||||
payment: 'Payment',
|
payment: 'Payment',
|
||||||
phone: 'Phone',
|
phone: 'Phone',
|
||||||
submit_form: 'Submit Form',
|
submit_form: 'Submit Form',
|
||||||
@@ -62,6 +63,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
initials: 'Iniciales',
|
initials: 'Iniciales',
|
||||||
@@ -124,6 +126,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
minimize: 'Minimizza',
|
||||||
text: 'Testo',
|
text: 'Testo',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
initials: 'Iniziali',
|
initials: 'Iniziali',
|
||||||
@@ -186,6 +189,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
minimize: 'Minimieren',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Unterschrift',
|
signature: 'Unterschrift',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
@@ -248,6 +252,7 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
minimize: 'Réduire',
|
||||||
text: 'Texte',
|
text: 'Texte',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initiales',
|
initials: 'Initiales',
|
||||||
@@ -310,6 +315,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
minimize: 'Zminimalizuj',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
initials: 'Inicjały',
|
initials: 'Inicjały',
|
||||||
@@ -372,6 +378,7 @@ const pl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
minimize: 'Зменшити',
|
||||||
text: 'Текст',
|
text: 'Текст',
|
||||||
signature: 'Підпис',
|
signature: 'Підпис',
|
||||||
initials: 'Ініціали',
|
initials: 'Ініціали',
|
||||||
@@ -434,6 +441,7 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
minimize: 'Minimalizovat',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
initials: 'Iniciály',
|
initials: 'Iniciály',
|
||||||
@@ -496,6 +504,7 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Assinatura',
|
signature: 'Assinatura',
|
||||||
initials: 'Iniciais',
|
initials: 'Iniciais',
|
||||||
@@ -558,6 +567,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
minimize: 'לקטן',
|
||||||
text: 'טקסט',
|
text: 'טקסט',
|
||||||
signature: 'חתימה',
|
signature: 'חתימה',
|
||||||
initials: 'ראשי תיקיות',
|
initials: 'ראשי תיקיות',
|
||||||
@@ -621,6 +631,7 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
minimize: 'Minimaliseren',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
signature: 'Handtekening',
|
signature: 'Handtekening',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<span
|
<span
|
||||||
v-if="isDrawInitials"
|
v-if="isDrawInitials"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('type_initials')"
|
:data-tip="t('type_initial')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
href="#"
|
href="#"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
@@ -231,7 +231,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateWrittenInitials (e) {
|
updateWrittenInitials (e) {
|
||||||
this.isInitialsStarted = true
|
this.isInitialsStarted = !!e.target.value
|
||||||
|
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
>{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
|
>{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature && field.preferences?.format !== 'typed'"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('draw_signature')"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="withTypedSignature"
|
v-else-if="withTypedSignature && field.preferences?.format !== 'typed' && field.preferences?.format !== 'drawn'"
|
||||||
class="tooltip ml-2"
|
class="tooltip ml-2"
|
||||||
:data-tip="t('type_text')"
|
:data-tip="t('type_text')"
|
||||||
>
|
>
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
v-if="field.preferences?.format !== 'typed'"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('take_photo')"
|
:data-tip="t('take_photo')"
|
||||||
>
|
>
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
>
|
>
|
||||||
<IconCamera :width="16" />
|
<IconCamera :width="16" />
|
||||||
<input
|
<input
|
||||||
|
:key="uploadImageInputKey"
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -78,7 +80,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
>
|
>
|
||||||
@@ -111,7 +113,7 @@
|
|||||||
id="signature_text_input"
|
id="signature_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
:required="field.required"
|
:required="field.required && !isSignatureStarted"
|
||||||
:placeholder="`${t('type_signature_here')}...`"
|
:placeholder="`${t('type_signature_here')}...`"
|
||||||
type="text"
|
type="text"
|
||||||
@input="updateWrittenSignature"
|
@input="updateWrittenSignature"
|
||||||
@@ -185,7 +187,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isSignatureStarted: !!this.previousValue,
|
isSignatureStarted: !!this.previousValue,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
isTextSignature: false
|
isTextSignature: this.field.preferences?.format === 'typed',
|
||||||
|
uploadImageInputKey: Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -269,7 +272,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateWrittenSignature (e) {
|
updateWrittenSignature (e) {
|
||||||
this.isSignatureStarted = true
|
this.isSignatureStarted = !!e.target.value
|
||||||
|
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
@@ -340,16 +343,18 @@ export default {
|
|||||||
const x = (canvasWidth - targetWidth) / 2
|
const x = (canvasWidth - targetWidth) / 2
|
||||||
const y = (canvasHeight - targetHeight) / 2
|
const y = (canvasHeight - targetHeight) / 2
|
||||||
|
|
||||||
context.clearRect(0, 0, canvasWidth, canvasHeight)
|
setTimeout(() => {
|
||||||
context.drawImage(img, x, y, targetWidth, targetHeight)
|
context.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||||
|
context.drawImage(img, x, y, targetWidth, targetHeight)
|
||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
|
|
||||||
event.target.value = null
|
this.uploadImageInputKey = Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else-if="editable"
|
v-else-if="editable"
|
||||||
class="pr-1"
|
class="pr-1"
|
||||||
title="Remove"
|
:title="t('remove')"
|
||||||
@click.prevent="$emit('remove')"
|
@click.prevent="$emit('remove')"
|
||||||
>
|
>
|
||||||
<IconX width="14" />
|
<IconX width="14" />
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<IconPencil
|
<IconPencil
|
||||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
title="Edit"
|
:title="t('edit')"
|
||||||
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
:width="iconWidth"
|
:width="iconWidth"
|
||||||
:stroke-width="iconStrokeWidth"
|
:stroke-width="iconStrokeWidth"
|
||||||
@@ -44,6 +44,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
IconPencil
|
IconPencil
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default {
|
|||||||
return this.previewImagesIndex[i] || {
|
return this.previewImagesIndex[i] || {
|
||||||
metadata: lazyloadMetadata,
|
metadata: lazyloadMetadata,
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
|
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="field && !field.areas.length"
|
v-if="field && !field.areas.length"
|
||||||
title="Draw"
|
:title="t('draw')"
|
||||||
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
@click="$emit('set-draw', { field })"
|
@click="$emit('set-draw', { field })"
|
||||||
>
|
>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Settings"
|
:title="t('settings')"
|
||||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconSettings
|
<IconSettings
|
||||||
@@ -142,6 +142,43 @@
|
|||||||
{{ t('format') }}
|
{{ t('format') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'signature'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
:placeholder="t('format')"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
|
||||||
|
@change="[field.preferences.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="any"
|
||||||
|
:selected="!field.preferences?.format || field.preferences.format === 'any'"
|
||||||
|
>
|
||||||
|
{{ t('any') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="drawn"
|
||||||
|
:selected="field.preferences?.format === 'drawn'"
|
||||||
|
>
|
||||||
|
{{ t('drawn') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="typed"
|
||||||
|
:selected="field.preferences?.format === 'typed'"
|
||||||
|
>
|
||||||
|
{{ t('typed') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('format') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'phone'"
|
v-if="field.type != 'phone'"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -245,7 +282,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="relative text-transparent group-hover:text-base-content pr-1"
|
class="relative text-transparent group-hover:text-base-content pr-1"
|
||||||
title="Remove"
|
:title="t('remove')"
|
||||||
@click="$emit('remove', field)"
|
@click="$emit('remove', field)"
|
||||||
>
|
>
|
||||||
<IconTrashX
|
<IconTrashX
|
||||||
@@ -284,7 +321,7 @@
|
|||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
title="Draw"
|
:title="t('draw')"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@click.prevent="$emit('set-draw', { field, option })"
|
@click.prevent="$emit('set-draw', { field, option })"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
|
<div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
|
||||||
<button
|
<button
|
||||||
title="Up"
|
:title="t('up')"
|
||||||
class="relative w-2"
|
class="relative w-2"
|
||||||
style="font-size: 10px; margin-bottom: -4px"
|
style="font-size: 10px; margin-bottom: -4px"
|
||||||
@click.prevent.stop="[move(submitter, -1), $refs.label.focus()] "
|
@click.prevent.stop="[move(submitter, -1), $refs.label.focus()] "
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
▲
|
▲
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
title="Down"
|
:title="t('down')"
|
||||||
class="relative w-2"
|
class="relative w-2"
|
||||||
style="font-size: 10px; margin-top: -4px"
|
style="font-size: 10px; margin-top: -4px"
|
||||||
@click.prevent.stop="[move(submitter, 1), $refs.label.focus()] "
|
@click.prevent.stop="[move(submitter, 1), $refs.label.focus()] "
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
|
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
|
||||||
@@ -123,6 +123,17 @@ export default {
|
|||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
fieldIconsSorted () {
|
||||||
|
if (this.fieldTypes.length) {
|
||||||
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
|
acc[type] = this.fieldIcons[type]
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
} else {
|
||||||
|
return this.fieldIcons
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
class="grid grid-cols-3 gap-1 pb-2"
|
class="grid grid-cols-3 gap-1 pb-2"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -213,6 +213,17 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
fieldIconsSorted () {
|
||||||
|
if (this.fieldTypes.length) {
|
||||||
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
|
acc[type] = this.fieldIcons[type]
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
} else {
|
||||||
|
return this.fieldIcons
|
||||||
|
}
|
||||||
|
},
|
||||||
submitterFields () {
|
submitterFields () {
|
||||||
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,9 +3,17 @@ const en = {
|
|||||||
sign_yourself: 'Sign Yourself',
|
sign_yourself: 'Sign Yourself',
|
||||||
set_signing_date: 'Set signing date',
|
set_signing_date: 'Set signing date',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
|
remove: 'Remove',
|
||||||
|
edit: 'Edit',
|
||||||
|
settings: 'Settings',
|
||||||
|
up: 'Up',
|
||||||
|
down: 'Down',
|
||||||
checked: 'Checked',
|
checked: 'Checked',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
|
any: 'Any',
|
||||||
|
drawn: 'Drawn',
|
||||||
|
typed: 'Typed',
|
||||||
draw_field_on_the_document: 'Draw {field} field on the document',
|
draw_field_on_the_document: 'Draw {field} field on the document',
|
||||||
click_to_upload: 'Click to upload',
|
click_to_upload: 'Click to upload',
|
||||||
or_drag_and_drop_files: 'or drag and drop files',
|
or_drag_and_drop_files: 'or drag and drop files',
|
||||||
@@ -42,6 +50,7 @@ const en = {
|
|||||||
eighteenth_party: 'Eighteenth Party',
|
eighteenth_party: 'Eighteenth Party',
|
||||||
nineteenth_party: 'Nineteenth Party',
|
nineteenth_party: 'Nineteenth Party',
|
||||||
twentieth_party: 'Twentieth Party',
|
twentieth_party: 'Twentieth Party',
|
||||||
|
draw: 'Draw',
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
or_add_field_without_drawing: 'Or add field without drawing',
|
or_add_field_without_drawing: 'Or add field without drawing',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Settings"
|
:title="t('settings')"
|
||||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconSettings
|
<IconSettings
|
||||||
@@ -167,7 +167,7 @@ export default {
|
|||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandStripe
|
IconBrandStripe
|
||||||
},
|
},
|
||||||
inject: ['backgroundColor', 'save', 'currencies'],
|
inject: ['backgroundColor', 'save', 'currencies', 't'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
|
|
||||||
user = submission.created_by_user || submitter.template.author
|
user = submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
if submitter.template.account.users.exists?(id: user.id) &&
|
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false
|
||||||
submission.preferences['send_email'] != false
|
if submitter.submission.submitters.map(&:email).exclude?(user.email) &&
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
|
||||||
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
bcc = submission.preferences['bcc_completed'].presence ||
|
bcc = submission.preferences['bcc_completed'].presence ||
|
||||||
submission.template.account.account_configs
|
submission.account.account_configs
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
||||||
|
|
||||||
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
NotSuccessStatus = Class.new(StandardError)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
@@ -23,8 +24,13 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
|
|||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
|
|
||||||
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
if resp.status.to_i >= 400 && attempt <= MAX_ATTEMPTS &&
|
||||||
raise NotSuccessStatus, resp.status.to_s
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
|
SendFormCompletedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submitter, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp.status.to_i
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
class SendFormStartedWebhookRequestJob < ApplicationJob
|
class SendFormStartedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
NotSuccessStatus = Class.new(StandardError)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
@@ -21,8 +22,13 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
|
|||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
|
|
||||||
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
if resp.status.to_i >= 400 && attempt <= MAX_ATTEMPTS &&
|
||||||
raise NotSuccessStatus, resp.status.to_s
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
|
SendFormStartedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submitter, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp.status.to_i
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
class SendFormViewedWebhookRequestJob < ApplicationJob
|
class SendFormViewedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
NotSuccessStatus = Class.new(StandardError)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
@@ -21,8 +22,13 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
|
|||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
|
|
||||||
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
if resp.status.to_i >= 400 && attempt <= MAX_ATTEMPTS &&
|
||||||
raise NotSuccessStatus, resp.status.to_s
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
|
SendFormViewedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submitter, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp.status.to_i
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -30,6 +30,10 @@ class Account < ApplicationRecord
|
|||||||
foreign_key: :linked_account_id,
|
foreign_key: :linked_account_id,
|
||||||
class_name: 'AccountLinkedAccount',
|
class_name: 'AccountLinkedAccount',
|
||||||
inverse_of: :linked_account
|
inverse_of: :linked_account
|
||||||
|
has_many :linked_account_accounts, dependent: :destroy,
|
||||||
|
foreign_key: :linked_account_id,
|
||||||
|
class_name: 'AccountLinkedAccount',
|
||||||
|
inverse_of: :linked_account
|
||||||
has_many :linked_accounts, through: :account_linked_accounts
|
has_many :linked_accounts, through: :account_linked_accounts
|
||||||
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
|
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
|
||||||
has_many :active_users, -> { active }, dependent: :destroy,
|
has_many :active_users, -> { active }, dependent: :destroy,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class Submission < ApplicationRecord
|
|||||||
def audit_trail_url
|
def audit_trail_url
|
||||||
return if audit_trail.blank?
|
return if audit_trail.blank?
|
||||||
|
|
||||||
Rails.application.routes.url_helpers.rails_storage_proxy_url(audit_trail, **Docuseal.default_url_options)
|
ActiveStorage::Blob.proxy_url(audit_trail.blob)
|
||||||
end
|
end
|
||||||
alias audit_log_url audit_trail_url
|
alias audit_log_url audit_trail_url
|
||||||
end
|
end
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
#
|
#
|
||||||
class UserConfig < ApplicationRecord
|
class UserConfig < ApplicationRecord
|
||||||
SIGNATURE_KEY = 'signature'
|
SIGNATURE_KEY = 'signature'
|
||||||
|
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
|||||||
@@ -123,26 +123,24 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if !Docuseal.multitenant? %>
|
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'multiple') %>
|
||||||
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'multiple') %>
|
<% if can?(:manage, account_config) %>
|
||||||
<% if can?(:manage, account_config) %>
|
<div class="px-1 mt-8 max-w-xl">
|
||||||
<div class="px-1 mt-8 max-w-xl">
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
<div class="flex justify-between items-end mb-4 mt-8">
|
<h2 class="text-3xl font-bold">Preferences</h2>
|
||||||
<h2 class="text-3xl font-bold">Preferences</h2>
|
|
||||||
</div>
|
|
||||||
<% if can?(:manage, account_config) %>
|
|
||||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
|
||||||
<%= f.hidden_field :key %>
|
|
||||||
<div class="flex items-center justify-between py-2.5">
|
|
||||||
<span>
|
|
||||||
Apply multiple PDF digital signatures in the document per each signer
|
|
||||||
</span>
|
|
||||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == 'multiple', onchange: 'this.form.requestSubmit()' }, 'multiple', 'single' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
Apply multiple PDF digital signatures in the document per each signer
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == 'multiple', onchange: 'this.form.requestSubmit()' }, 'multiple', 'single' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1 +1 @@
|
|||||||
<%= render 'docuseal_logo' %>
|
<%= render 'start_form/docuseal_logo' %>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div dir="auto" class="text-center text-2xl font-semibold">
|
<div dir="auto" class="text-center text-2xl font-semibold">
|
||||||
<%= t('form_has_been_submitted_already') %>
|
<%= t('form_has_been_submitted_already') %>
|
||||||
</div>
|
</div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
<% if Accounts.can_send_emails?(@submitter.account) %>
|
||||||
<div>
|
<div>
|
||||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
|
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,4 +31,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -37,4 +37,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
<% if @submission.audit_trail.present? %>
|
<% if @submission.audit_trail.present? %>
|
||||||
<a href="<%= rails_blob_path(@submission.audit_trail) %>" class="white-button" target="_blank">
|
<a href="<%= ActiveStorage::Blob.proxy_url(@submission.audit_trail.blob) %>" class="white-button" target="_blank">
|
||||||
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
Audit Log
|
Audit Log
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
||||||
<% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
<% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||||
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
||||||
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
<img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
||||||
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
||||||
<%= item['name'].presence || document.filename.base %>
|
<%= item['name'].presence || document.filename.base %>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,9 +53,9 @@
|
|||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.first.metadata %>
|
<% lazyload_metadata = document.preview_images.first.metadata %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
||||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="border rounded mb-4" height="<%= page.metadata['height'] %>">
|
<img loading="lazy" src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, index].join(':'), expires_in: 10.minutes) { page.url } %>" width="<%= page.metadata['width'] %>" class="border rounded mb-4" height="<%= page.metadata['height'] %>">
|
||||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% document_annots_index[index]&.each do |annot| %>
|
<% document_annots_index[index]&.each do |annot| %>
|
||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
@@ -144,10 +144,10 @@
|
|||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<% if field['type'].in?(%w[signature initials]) %>
|
<% if field['type'].in?(%w[signature initials]) %>
|
||||||
<div class="w-full bg-base-300 py-1">
|
<div class="w-full bg-base-300 py-1">
|
||||||
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(['image', 'stamp']) %>
|
<% elsif field['type'].in?(['image', 'stamp']) %>
|
||||||
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
|
<div class="space-y-6 mx-auto">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<%= render 'start_form/docuseal_logo' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-3">
|
||||||
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p dir="auto" class="text-lg font-bold mb-1"><%= @submission.template.name %></p>
|
||||||
|
<% if submitter = @submission.submitters.map(&:completed_at).max %>
|
||||||
|
<p dir="auto" class="text-sm"><%= t('signed_on_time', time: l(submitter.to_date, format: :long)) %></p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<div>
|
||||||
|
<%= link_to 'Create free account', registration_path, class: 'white-button w-full' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= render 'shared/attribution', link_path: '/start', account: @submission.account %>
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
|
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
|
||||||
<p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.submission.template.account.name) %></p>
|
<p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.account.name) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) && @submitter.email.present? %>
|
<% if Accounts.can_send_emails?(@submitter.account) && @submitter.email.present? %>
|
||||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
<% if @submitter.submission.template.submitters.size != 1 %>
|
<% if @submitter.submission.template.submitters.size != 1 %>
|
||||||
<div class="divider uppercase"><%= t('or') %></div>
|
<div class="divider uppercase"><%= t('or') %></div>
|
||||||
@@ -25,18 +25,20 @@
|
|||||||
<div class="py-2"></div>
|
<div class="py-2"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
<% if @submitter.completed_at > 15.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<span><%= t('download_documents') %></span>
|
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||||
</span>
|
<span><%= t('download_documents') %></span>
|
||||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
</span>
|
||||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
<span><%= t('downloading') %></span>
|
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||||
</span>
|
<span><%= t('downloading') %></span>
|
||||||
</download-button>
|
</span>
|
||||||
|
</download-button>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% if @submitter.submission.template.submitters.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
|
<% if @submitter.submission.template.submitters.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && !@submitter.template.archived_at? %>
|
||||||
<div class="divider uppercase"><%= t('or') %></div>
|
<div class="divider uppercase"><%= t('or') %></div>
|
||||||
<div>
|
<div>
|
||||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
@@ -44,4 +46,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.last.metadata %>
|
<% lazyload_metadata = document.preview_images.last.metadata %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
||||||
<div class="relative my-4 shadow-md">
|
<div class="relative my-4 shadow-md">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
|
||||||
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
@@ -24,13 +24,13 @@
|
|||||||
<% value = values[field['uuid']].presence || (field['readonly'] ? 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'] ? 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 %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.account.locale %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,11 +12,16 @@
|
|||||||
</a>
|
</a>
|
||||||
<folder-autocomplete class="flex justify-between w-full">
|
<folder-autocomplete class="flex justify-between w-full">
|
||||||
<input id="folder_name" placeholder="Folder Name" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
|
<input id="folder_name" placeholder="Folder Name" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
|
||||||
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5">
|
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5">
|
||||||
Change Folder
|
Change Folder
|
||||||
</a>
|
</a>
|
||||||
</folder-autocomplete>
|
</folder-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
|
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.account_linked_accounts.exists?(account_type: :linked) %>
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts.where.not(id: true_user.account.testing_accounts)].map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: @base_template ? 'Submit' : 'Create', disabled_with: 'Creating'), class: 'base-button' %>
|
<%= f.button button_title(title: @base_template ? 'Submit' : 'Create', disabled_with: 'Creating'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ module DocuSeal
|
|||||||
|
|
||||||
config.active_storage.routes_prefix = ''
|
config.active_storage.routes_prefix = ''
|
||||||
|
|
||||||
|
config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true'
|
||||||
|
|
||||||
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE es it de fr pl uk cs pt he nl]
|
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE es it de fr pl uk cs pt he nl]
|
||||||
config.i18n.fallbacks = [:en]
|
config.i18n.fallbacks = [:en]
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ Rails.application.configure do
|
|||||||
end
|
end
|
||||||
|
|
||||||
config.active_storage.resolve_model_to_route = :rails_storage_proxy if ENV['ACTIVE_STORAGE_PUBLIC'] != 'true'
|
config.active_storage.resolve_model_to_route = :rails_storage_proxy if ENV['ACTIVE_STORAGE_PUBLIC'] != 'true'
|
||||||
|
config.active_storage.service_urls_expire_in = 15.minutes
|
||||||
|
|
||||||
# Mount Action Cable outside main process or domain.
|
# Mount Action Cable outside main process or domain.
|
||||||
# config.action_cable.mount_path = nil
|
# config.action_cable.mount_path = nil
|
||||||
@@ -107,7 +108,7 @@ Rails.application.configure do
|
|||||||
logger.formatter = config.log_formatter
|
logger.formatter = config.log_formatter
|
||||||
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
||||||
|
|
||||||
encryption_secret = Digest::SHA256.hexdigest(ENV['SECRET_KEY_BASE'].to_s)
|
encryption_secret = ENV['ENCRYPTION_SECRET'].presence || Digest::SHA256.hexdigest(ENV['SECRET_KEY_BASE'].to_s)
|
||||||
|
|
||||||
config.active_record.encryption = {
|
config.active_record.encryption = {
|
||||||
primary_key: encryption_secret.first(32),
|
primary_key: encryption_secret.first(32),
|
||||||
|
|||||||
@@ -5,8 +5,43 @@ ActiveSupport.on_load(:active_storage_attachment) do
|
|||||||
|
|
||||||
has_many_attached :preview_images
|
has_many_attached :preview_images
|
||||||
|
|
||||||
|
def signed_uuid
|
||||||
|
@signed_uuid ||= ApplicationRecord.signed_id_verifier.generate(uuid, expires_in: 6.hours, purpose: :attachment)
|
||||||
|
end
|
||||||
|
|
||||||
def preview_image_url
|
def preview_image_url
|
||||||
preview_images.joins(:blob).find_by(blob: { filename: '0.jpg' })&.url
|
first_page = preview_images.joins(:blob).find_by(blob: { filename: '0.jpg' })
|
||||||
|
|
||||||
|
return unless first_page
|
||||||
|
|
||||||
|
ActiveStorage::Blob.proxy_url(first_page.blob)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveSupport.on_load(:active_storage_blob) do
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
|
def self.proxy_url(blob, expires_in: nil)
|
||||||
|
Rails.application.routes.url_helpers.blobs_proxy_url(
|
||||||
|
signed_uuid: blob.signed_uuid(expires_in:), filename: blob.filename,
|
||||||
|
**Docuseal.default_url_options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def uuid
|
||||||
|
super || begin
|
||||||
|
new_uuid = SecureRandom.uuid
|
||||||
|
update_columns(uuid: new_uuid)
|
||||||
|
new_uuid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_uuid(expires_in: nil)
|
||||||
|
ApplicationRecord.signed_id_verifier.generate([uuid, 'blob'], expires_in:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
service.delete(key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ Devise.setup do |config|
|
|||||||
# This can reduce the time taken to boot the app but if your application
|
# This can reduce the time taken to boot the app but if your application
|
||||||
# requires the Devise mappings to be loaded during boot time the application
|
# requires the Devise mappings to be loaded during boot time the application
|
||||||
# won't boot properly.
|
# won't boot properly.
|
||||||
# config.reload_routes = true
|
config.reload_routes = false
|
||||||
|
|
||||||
# ==> Configuration for :database_authenticatable
|
# ==> Configuration for :database_authenticatable
|
||||||
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
|
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
|
||||||
|
|||||||
@@ -6,7 +6,22 @@ if defined?(Rollbar)
|
|||||||
Rollbar.configure do |config|
|
Rollbar.configure do |config|
|
||||||
config.access_token = ENV.fetch('ROLLBAR_ACCESS_TOKEN', nil)
|
config.access_token = ENV.fetch('ROLLBAR_ACCESS_TOKEN', nil)
|
||||||
|
|
||||||
|
config.transform << proc do |options|
|
||||||
|
data = options[:payload]['data']
|
||||||
|
|
||||||
|
if data[:request]
|
||||||
|
data[:request][:cookies] = {}
|
||||||
|
data[:request][:session] = {}
|
||||||
|
data[:request][:url] =
|
||||||
|
data[:request][:url].to_s.sub(%r{(/[sde]/)\w{8}}, '\1********').sub(/\A(.*?)--(.*)/, '\1--********')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
config.enabled = true
|
config.enabled = true
|
||||||
|
config.collect_user_ip = false
|
||||||
|
config.anonymize_user_ip = true
|
||||||
|
config.scrub_headers += %w[X-Auth-Token Cookie X-Csrf-Token Referer]
|
||||||
|
config.scrub_fields += %i[slug uuid attachment_uuid]
|
||||||
|
|
||||||
config.exception_level_filters['ActionController::RoutingError'] = 'ignore'
|
config.exception_level_filters['ActionController::RoutingError'] = 'ignore'
|
||||||
|
|
||||||
|
|||||||
+17
-2
@@ -53,6 +53,7 @@ Rails.application.routes.draw do
|
|||||||
resources :verify_pdf_signature, only: %i[create]
|
resources :verify_pdf_signature, only: %i[create]
|
||||||
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
|
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
|
||||||
resources :account_configs, only: %i[create]
|
resources :account_configs, only: %i[create]
|
||||||
|
resources :encrypted_user_configs, only: %i[destroy]
|
||||||
resources :timestamp_server, only: %i[create]
|
resources :timestamp_server, only: %i[create]
|
||||||
resources :dashboard, only: %i[index]
|
resources :dashboard, only: %i[index]
|
||||||
resources :setup, only: %i[index create]
|
resources :setup, only: %i[index create]
|
||||||
@@ -80,7 +81,19 @@ Rails.application.routes.draw do
|
|||||||
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
||||||
resources :submissions_export, only: %i[index new]
|
resources :submissions_export, only: %i[index new]
|
||||||
end
|
end
|
||||||
resources :preview_document_page, only: %i[show], path: '/preview/:attachment_uuid'
|
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'
|
||||||
|
resource :blobs_proxy, only: %i[show], path: '/blobs_proxy/:signed_uuid/*filename',
|
||||||
|
controller: 'api/active_storage_blobs_proxy'
|
||||||
|
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
resource :blobs_proxy_legacy, only: %i[show],
|
||||||
|
path: '/blobs/proxy/:signed_id/*filename',
|
||||||
|
controller: 'api/active_storage_blobs_proxy_legacy',
|
||||||
|
as: :rails_blob
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
|
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
|
||||||
get :completed
|
get :completed
|
||||||
@@ -94,7 +107,9 @@ Rails.application.routes.draw do
|
|||||||
get :completed
|
get :completed
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug'
|
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug' do
|
||||||
|
get :completed
|
||||||
|
end
|
||||||
|
|
||||||
resources :send_submission_email, only: %i[create] do
|
resources :send_submission_email, only: %i[create] do
|
||||||
get :success, on: :collection
|
get :success, on: :collection
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddUuidToActiveStorageBlobs < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :active_storage_blobs, :uuid, :string
|
||||||
|
|
||||||
|
add_index :active_storage_blobs, :uuid, unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
+3
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_02_03_113455) do
|
ActiveRecord::Schema[7.1].define(version: 2024_02_18_082157) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
@@ -74,7 +74,9 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_03_113455) do
|
|||||||
t.bigint "byte_size", null: false
|
t.bigint "byte_size", null: false
|
||||||
t.string "checksum"
|
t.string "checksum"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
t.string "uuid"
|
||||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
||||||
|
t.index ["uuid"], name: "index_active_storage_blobs_on_uuid", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Docuseal
|
module Docuseal
|
||||||
|
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
|
||||||
PRODUCT_URL = 'https://www.docuseal.co'
|
PRODUCT_URL = 'https://www.docuseal.co'
|
||||||
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
||||||
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
|
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module Params
|
|||||||
|
|
||||||
validator.call
|
validator.call
|
||||||
rescue InvalidParameterError => e
|
rescue InvalidParameterError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
raise e unless validator.dry_run?
|
raise e unless validator.dry_run?
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
|||||||
@@ -70,6 +70,18 @@ class PdfProcessor < HexaPDF::Content::Processor
|
|||||||
page.contents = process_handler_instance.contents
|
page.contents = process_handler_instance.contents
|
||||||
end
|
end
|
||||||
|
|
||||||
|
page[:Annots].to_a.each do |annot|
|
||||||
|
next unless annot
|
||||||
|
|
||||||
|
text = annot[:Contents].to_s.squish
|
||||||
|
|
||||||
|
next unless text.starts_with?('{{') && text.ends_with?('}}')
|
||||||
|
|
||||||
|
result_handler.call({ text:, rect: annot[:Rect] }, page, acc)
|
||||||
|
|
||||||
|
page[:Annots].delete(annot)
|
||||||
|
end
|
||||||
|
|
||||||
process_handler_instance.result.each do |item|
|
process_handler_instance.result.each do |item|
|
||||||
result_handler.call(item, page, acc)
|
result_handler.call(item, page, acc)
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false, params: {})
|
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false, params: {})
|
||||||
preferences = Submitters.normalize_preferences(template.account, user, params)
|
preferences = Submitters.normalize_preferences(user.account, user, params)
|
||||||
|
|
||||||
parse_emails(emails).uniq.map do |email|
|
parse_emails(emails).uniq.map do |email|
|
||||||
submission = template.submissions.new(created_by_user: user,
|
submission = template.submissions.new(created_by_user: user,
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ module Submissions
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false, params: {})
|
def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false, params: {})
|
||||||
preferences = Submitters.normalize_preferences(template.account, user, params)
|
preferences = Submitters.normalize_preferences(user.account, user, params)
|
||||||
|
|
||||||
Array.wrap(submissions_attrs).map do |attrs|
|
Array.wrap(submissions_attrs).map do |attrs|
|
||||||
submission_preferences = Submitters.normalize_preferences(template.account, user, attrs)
|
submission_preferences = Submitters.normalize_preferences(user.account, user, attrs)
|
||||||
submission_preferences = preferences.merge(submission_preferences)
|
submission_preferences = preferences.merge(submission_preferences)
|
||||||
|
|
||||||
set_submission_preferences = submission_preferences.slice('send_email', 'bcc_completed')
|
set_submission_preferences = submission_preferences.slice('send_email', 'bcc_completed')
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ module Submissions
|
|||||||
module GenerateAuditTrail
|
module GenerateAuditTrail
|
||||||
FONT_SIZE = 9
|
FONT_SIZE = 9
|
||||||
TEXT_COLOR = '525252'
|
TEXT_COLOR = '525252'
|
||||||
FONT_PATH = '/fonts/LiberationSans-Regular.ttf'
|
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
|
||||||
FONT_BOLD_PATH = '/fonts/LiberationSans-Bold.ttf'
|
FONT_BOLD_PATH = '/fonts/GoNotoKurrent-Bold.ttf'
|
||||||
FONT_NAME = if File.exist?(FONT_PATH)
|
FONT_NAME = if File.exist?(FONT_PATH)
|
||||||
FONT_PATH
|
FONT_PATH
|
||||||
else
|
else
|
||||||
@@ -28,6 +28,7 @@ module Submissions
|
|||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
RTL_REGEXP = TextUtils::RTL_REGEXP
|
RTL_REGEXP = TextUtils::RTL_REGEXP
|
||||||
|
MAX_IMAGE_HEIGHT = 100
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ module Submissions
|
|||||||
style.frame = style.create_frame(canvas.context, 50)
|
style.frame = style.create_frame(canvas.context, 50)
|
||||||
end
|
end
|
||||||
|
|
||||||
composer.style(:base, font: FONT_NAME, font_size: FONT_SIZE, fill_color: TEXT_COLOR, line_spacing: 1.2)
|
composer.style(:base, font: FONT_NAME, font_size: FONT_SIZE, fill_color: TEXT_COLOR, line_spacing: 1)
|
||||||
composer.style(:link, fill_color: 'hp-blue-light', underline: true)
|
composer.style(:link, fill_color: 'hp-blue-light', underline: true)
|
||||||
|
|
||||||
composer.new_page
|
composer.new_page
|
||||||
@@ -71,16 +72,16 @@ module Submissions
|
|||||||
|
|
||||||
column.text(account.linked_account_account&.testing? ? 'Testing Log - Not for Production Use' : 'Audit Log',
|
column.text(account.linked_account_account&.testing? ? 'Testing Log - Not for Production Use' : 'Audit Log',
|
||||||
font_size: 16,
|
font_size: 16,
|
||||||
padding: [15, 0, 0, 0],
|
padding: [10, 0, 0, 0],
|
||||||
position: :float, position_hint: :right)
|
position: :float, text_align: :right)
|
||||||
end
|
end
|
||||||
|
|
||||||
composer.column(columns: 1) do |column|
|
composer.column(columns: 1) do |column|
|
||||||
column.text("Envelope ID: #{submission.id}", font_size: 12, padding: [20, 0, 10, 0], position: :float)
|
column.text("Envelope ID: #{submission.id}", font_size: 12, padding: [15, 0, 8, 0], position: :float)
|
||||||
|
|
||||||
unless submission.source.in?(%w[embed api])
|
unless submission.source.in?(%w[embed api])
|
||||||
column.formatted_text([{ link: verify_url, text: 'Verify', style: :link }],
|
column.formatted_text([{ link: verify_url, text: 'Verify', style: :link }],
|
||||||
font_size: 9, padding: [22, 0, 10, 0], position: :float, align: :right)
|
font_size: 9, padding: [15, 0, 10, 0], position: :float, text_align: :right)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
link =
|
link =
|
||||||
Rails.application.routes.url_helpers.rails_blob_url(document, **Docuseal.default_url_options)
|
ActiveStorage::Blob.proxy_url(document.blob)
|
||||||
|
|
||||||
[
|
[
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
@@ -114,13 +115,13 @@ module Submissions
|
|||||||
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
|
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
|
||||||
"#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
"#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
||||||
"#{TimeUtils.timezone_abbr(account.timezone, document.created_at)}"
|
"#{TimeUtils.timezone_abbr(account.timezone, document.created_at)}"
|
||||||
], line_spacing: 1.8
|
], line_spacing: 1.3
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
if documents_data.present?
|
if documents_data.present?
|
||||||
composer.table(documents_data, cell_style: { padding: [0, 0, 25, 0], border: { width: 0 } })
|
composer.table(documents_data, cell_style: { padding: [0, 0, 20, 0], border: { width: 0 } })
|
||||||
|
|
||||||
composer.draw_box(divider)
|
composer.draw_box(divider)
|
||||||
end
|
end
|
||||||
@@ -151,7 +152,7 @@ module Submissions
|
|||||||
submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] },
|
submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] },
|
||||||
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
|
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
|
||||||
submitter.phone && { text: "#{submitter.phone}\n" }
|
submitter.phone && { text: "#{submitter.phone}\n" }
|
||||||
].compact_blank, line_spacing: 1.8, padding: [0, 20, 0, 0]
|
].compact_blank, line_spacing: 1.3, padding: [0, 20, 0, 0]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -167,7 +168,7 @@ module Submissions
|
|||||||
completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" },
|
completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" },
|
||||||
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
|
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
|
||||||
"\n"
|
"\n"
|
||||||
].compact_blank, line_spacing: 1.8, padding: [10, 20, 20, 0]
|
].compact_blank, line_spacing: 1.3, padding: [10, 20, 20, 0]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -192,8 +193,8 @@ module Submissions
|
|||||||
font_size: 6
|
font_size: 6
|
||||||
}
|
}
|
||||||
].compact_blank,
|
].compact_blank,
|
||||||
align: field['name'].to_s.match?(RTL_REGEXP) ? :right : :left,
|
text_align: field['name'].to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
line_spacing: 1.8, padding: [0, 0, 5, 0]
|
line_spacing: 1.3, padding: [0, 0, 2, 0]
|
||||||
),
|
),
|
||||||
if field['type'].in?(%w[image signature initials stamp])
|
if field['type'].in?(%w[image signature initials stamp])
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
@@ -207,11 +208,16 @@ module Submissions
|
|||||||
width = field['type'] == 'initials' ? 100 : 200
|
width = field['type'] == 'initials' ? 100 : 200
|
||||||
height = resized_image.height * (width.to_f / resized_image.width)
|
height = resized_image.height * (width.to_f / resized_image.width)
|
||||||
|
|
||||||
composer.image(io, width:, height:, margin: [0, 0, 10, 0])
|
if height > MAX_IMAGE_HEIGHT
|
||||||
|
width = (MAX_IMAGE_HEIGHT / height) * width
|
||||||
|
height = MAX_IMAGE_HEIGHT
|
||||||
|
end
|
||||||
|
|
||||||
|
composer.image(io, width:, height:, margin: [5, 0, 10, 0])
|
||||||
composer.formatted_text_box([{ text: '' }])
|
composer.formatted_text_box([{ text: '' }])
|
||||||
elsif field['type'].in?(%w[file payment])
|
elsif field['type'].in?(%w[file payment])
|
||||||
if field['type'] == 'payment'
|
if field['type'] == 'payment'
|
||||||
unit = CURRENCY_SYMBOLS[field['preferences']['currency']]
|
unit = CURRENCY_SYMBOLS[field['preferences']['currency']] || field['preferences']['currency']
|
||||||
|
|
||||||
price = ApplicationController.helpers.number_to_currency(field['preferences']['price'], unit:)
|
price = ApplicationController.helpers.number_to_currency(field['preferences']['price'], unit:)
|
||||||
|
|
||||||
@@ -222,7 +228,7 @@ module Submissions
|
|||||||
Array.wrap(value).map do |uuid|
|
Array.wrap(value).map do |uuid|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
link =
|
link =
|
||||||
Rails.application.routes.url_helpers.rails_blob_url(attachment, **Docuseal.default_url_options)
|
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||||
|
|
||||||
{ link:, text: "#{attachment.filename}\n", style: :link }
|
{ link:, text: "#{attachment.filename}\n", style: :link }
|
||||||
end,
|
end,
|
||||||
@@ -239,7 +245,7 @@ module Submissions
|
|||||||
value = Submissions::GenerateResultAttachments::REPLACE_EMOJI[value] || value
|
value = Submissions::GenerateResultAttachments::REPLACE_EMOJI[value] || value
|
||||||
|
|
||||||
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
|
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
|
||||||
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
padding: [0, 0, 10, 0])
|
padding: [0, 0, 10, 0])
|
||||||
end
|
end
|
||||||
]
|
]
|
||||||
@@ -270,7 +276,7 @@ module Submissions
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
composer.table(events_data, cell_style: { padding: [0, 0, 20, 0], border: { width: 0 } }) if events_data.present?
|
composer.table(events_data, cell_style: { padding: [0, 0, 12, 0], border: { width: 0 } }) if events_data.present?
|
||||||
|
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
@@ -308,8 +314,8 @@ module Submissions
|
|||||||
font_size: 20,
|
font_size: 20,
|
||||||
font: [FONT_BOLD_NAME, { variant: :bold }],
|
font: [FONT_BOLD_NAME, { variant: :bold }],
|
||||||
width: 100,
|
width: 100,
|
||||||
padding: [12, 0, 0, 8],
|
padding: [5, 0, 0, 8],
|
||||||
position: :float, position_hint: :left)
|
position: :float, text_align: :left)
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ module Submissions
|
|||||||
submission_data += submitter.documents.map.with_index(1) do |attachment, index|
|
submission_data += submitter.documents.map.with_index(1) do |attachment, index|
|
||||||
{
|
{
|
||||||
name: "Document #{index}",
|
name: "Document #{index}",
|
||||||
value: attachment.url
|
value: ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -131,9 +131,13 @@ module Submissions
|
|||||||
|
|
||||||
value =
|
value =
|
||||||
if template_field_type.in?(%w[image signature])
|
if template_field_type.in?(%w[image signature])
|
||||||
attachments_index[submitter_value]&.url
|
attachment = attachments_index[submitter_value]
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
||||||
elsif template_field_type == 'file'
|
elsif template_field_type == 'file'
|
||||||
Array.wrap(submitter_value).compact_blank.filter_map { |e| attachments_index[e]&.url }
|
Array.wrap(submitter_value).compact_blank.filter_map do |e|
|
||||||
|
attachment = attachments_index[e]
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
||||||
|
end
|
||||||
else
|
else
|
||||||
submitter_value
|
submitter_value
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Submissions
|
module Submissions
|
||||||
module GenerateResultAttachments
|
module GenerateResultAttachments
|
||||||
FONT_SIZE = 11
|
FONT_SIZE = 11
|
||||||
FONT_PATH = '/fonts/LiberationSans-Regular.ttf'
|
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
|
||||||
FONT_NAME = if File.exist?(FONT_PATH)
|
FONT_NAME = if File.exist?(FONT_PATH)
|
||||||
FONT_PATH
|
FONT_PATH
|
||||||
else
|
else
|
||||||
@@ -22,14 +22,15 @@ module Submissions
|
|||||||
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
|
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
|
||||||
|
|
||||||
REPLACE_EMOJI = {
|
REPLACE_EMOJI = {
|
||||||
'✔️' => 'V'
|
'✔️' => 'V',
|
||||||
|
'✔' => 'V'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def call(submitter)
|
def call(submitter)
|
||||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, align: :center)
|
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
||||||
|
|
||||||
template = submitter.submission.template
|
template = submitter.submission.template
|
||||||
|
|
||||||
@@ -64,8 +65,8 @@ module Submissions
|
|||||||
|
|
||||||
value = submitter.values[field['uuid']]
|
value = submitter.values[field['uuid']]
|
||||||
|
|
||||||
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center,
|
layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center,
|
||||||
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
font: pdf.fonts.add(FONT_NAME), font_size:)
|
font: pdf.fonts.add(FONT_NAME), font_size:)
|
||||||
|
|
||||||
next if Array.wrap(value).compact_blank.blank?
|
next if Array.wrap(value).compact_blank.blank?
|
||||||
@@ -130,7 +131,7 @@ module Submissions
|
|||||||
height - (area['y'] * height) - lines[..next_index].sum(&:height) + height_diff
|
height - (area['y'] * height) - lines[..next_index].sum(&:height) + height_diff
|
||||||
],
|
],
|
||||||
A: { Type: :Action, S: :URI,
|
A: { Type: :Action, S: :URI,
|
||||||
URI: h.rails_blob_url(attachment, **Docuseal.default_url_options) }
|
URI: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -340,8 +341,6 @@ module Submissions
|
|||||||
def fetch_sign_reason(submitter)
|
def fetch_sign_reason(submitter)
|
||||||
reason_name = submitter.email || submitter.name || submitter.phone
|
reason_name = submitter.email || submitter.name || submitter.phone
|
||||||
|
|
||||||
return sign_reason(reason_name) if Docuseal.multitenant?
|
|
||||||
|
|
||||||
config =
|
config =
|
||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
AccountConfig.where(account: submitter.account, key: AccountConfig::ESIGNING_PREFERENCE_KEY)
|
AccountConfig.where(account: submitter.account, key: AccountConfig::ESIGNING_PREFERENCE_KEY)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module GenerateFontImage
|
||||||
|
WIDTH = 3000
|
||||||
|
HEIGHT = 160
|
||||||
|
|
||||||
|
FONTS = {
|
||||||
|
'Dancing Script Regular' => '/fonts/DancingScript-Regular.otf',
|
||||||
|
'Go Noto Kurrent-Bold Bold' => '/fonts/GoNotoKurrent-Bold.ttf'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
FONT_ALIASES = {
|
||||||
|
'initials' => 'Go Noto Kurrent-Bold Bold',
|
||||||
|
'signature' => 'Dancing Script Regular'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(text, font: nil)
|
||||||
|
font = FONT_ALIASES[font] || font
|
||||||
|
|
||||||
|
text_image = Vips::Image.text(text, font:, fontfile: FONTS[font],
|
||||||
|
width: WIDTH, height: HEIGHT, wrap: :none)
|
||||||
|
|
||||||
|
text_mask = Vips::Image.black(text_image.width, text_image.height)
|
||||||
|
|
||||||
|
text_mask.bandjoin(text_image).copy(interpretation: :b_w).write_to_buffer('.png')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,8 +4,11 @@ module Submitters
|
|||||||
module NormalizeValues
|
module NormalizeValues
|
||||||
CHECKSUM_CACHE_STORE = ActiveSupport::Cache::MemoryStore.new
|
CHECKSUM_CACHE_STORE = ActiveSupport::Cache::MemoryStore.new
|
||||||
|
|
||||||
UnknownFieldName = Class.new(StandardError)
|
BaseError = Class.new(StandardError)
|
||||||
UnknownSubmitterName = Class.new(StandardError)
|
|
||||||
|
UnknownFieldName = Class.new(BaseError)
|
||||||
|
InvalidDefaultValue = Class.new(BaseError)
|
||||||
|
UnknownSubmitterName = Class.new(BaseError)
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -31,7 +34,8 @@ module Submitters
|
|||||||
field = fields_uuid_index[key]
|
field = fields_uuid_index[key]
|
||||||
|
|
||||||
if field['type'].in?(%w[initials signature image file]) && value.present?
|
if field['type'].in?(%w[initials signature image file]) && value.present?
|
||||||
new_value, new_attachments = normalize_attachment_value(value, template.account, for_submitter)
|
new_value, new_attachments =
|
||||||
|
normalize_attachment_value(value, field['type'], template.account, attachments, for_submitter)
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
attachments.push(*new_attachments)
|
||||||
|
|
||||||
@@ -57,7 +61,7 @@ module Submitters
|
|||||||
value
|
value
|
||||||
end
|
end
|
||||||
rescue Date::Error => e
|
rescue Date::Error => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
@@ -66,7 +70,8 @@ module Submitters
|
|||||||
if submitter_name
|
if submitter_name
|
||||||
submitter =
|
submitter =
|
||||||
template.submitters.find { |e| e['name'] == submitter_name } ||
|
template.submitters.find { |e| e['name'] == submitter_name } ||
|
||||||
raise(UnknownSubmitterName, "Unknown submitter role: #{submitter_name}")
|
raise(UnknownSubmitterName,
|
||||||
|
"Unknown submitter role: #{submitter_name}. Template defines #{template.submitters.pluck('name')}")
|
||||||
end
|
end
|
||||||
|
|
||||||
fields = for_submitter&.submission&.template_fields || template.fields
|
fields = for_submitter&.submission&.template_fields || template.fields
|
||||||
@@ -86,20 +91,37 @@ module Submitters
|
|||||||
.merge(fields.index_by { |e| e['name'].to_s.downcase })
|
.merge(fields.index_by { |e| e['name'].to_s.downcase })
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_attachment_value(value, account, for_submitter = nil)
|
def normalize_attachment_value(value, type, account, attachments, for_submitter = nil)
|
||||||
if value.is_a?(Array)
|
if value.is_a?(Array)
|
||||||
new_attachments = value.map { |v| find_or_build_attachment(v, account, for_submitter) }
|
new_attachments = value.map do |v|
|
||||||
|
new_attachment = find_or_build_attachment(v, type, account, for_submitter)
|
||||||
|
|
||||||
|
attachments.find { |a| a.blob_id == new_attachment.blob_id } || new_attachment
|
||||||
|
end
|
||||||
|
|
||||||
[new_attachments.map(&:uuid), new_attachments]
|
[new_attachments.map(&:uuid), new_attachments]
|
||||||
else
|
else
|
||||||
new_attachment = find_or_build_attachment(value, account, for_submitter)
|
new_attachment = find_or_build_attachment(value, type, account, for_submitter)
|
||||||
|
|
||||||
[new_attachment.uuid, new_attachment]
|
existing_attachment = attachments.find { |a| a.blob_id == new_attachment.blob_id }
|
||||||
|
|
||||||
|
attachment = existing_attachment || new_attachment
|
||||||
|
|
||||||
|
[attachment.uuid, attachment]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_build_attachment(value, account, for_submitter = nil)
|
def find_or_build_attachment(value, type, account, for_submitter = nil)
|
||||||
blob = find_or_create_blobs(account, value)
|
blob =
|
||||||
|
if value.match?(%r{\Ahttps?://})
|
||||||
|
find_or_create_blob_from_url(account, value)
|
||||||
|
elsif type.in?(%w[signature initials]) && value.length < 50
|
||||||
|
find_or_create_blob_from_text(account, value, type)
|
||||||
|
elsif (data = Base64.decode64(value)) && Marcel::MimeType.for(data).include?('image')
|
||||||
|
find_or_create_blob_from_base64(account, data, type)
|
||||||
|
else
|
||||||
|
raise InvalidDefaultValue, "Invalid value, url, base64 or text < 50 chars is expected: #{value.first(50)}..."
|
||||||
|
end
|
||||||
|
|
||||||
attachment = for_submitter.attachments.find_by(blob_id: blob.id) if for_submitter
|
attachment = for_submitter.attachments.find_by(blob_id: blob.id) if for_submitter
|
||||||
|
|
||||||
@@ -111,7 +133,31 @@ module Submitters
|
|||||||
attachment
|
attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_blobs(account, url)
|
def find_or_create_blob_from_base64(account, data, type)
|
||||||
|
checksum = Digest::MD5.base64digest(data)
|
||||||
|
|
||||||
|
blob = find_blob_by_checksum(checksum, account)
|
||||||
|
|
||||||
|
blob || ActiveStorage::Blob.create_and_upload!(
|
||||||
|
io: StringIO.new(data),
|
||||||
|
filename: "#{type}.png"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create_blob_from_text(account, text, type)
|
||||||
|
data = Submitters::GenerateFontImage.call(text, font: type)
|
||||||
|
|
||||||
|
checksum = Digest::MD5.base64digest(data)
|
||||||
|
|
||||||
|
blob = find_blob_by_checksum(checksum, account)
|
||||||
|
|
||||||
|
blob || ActiveStorage::Blob.create_and_upload!(
|
||||||
|
io: StringIO.new(data),
|
||||||
|
filename: "#{type}.png"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create_blob_from_url(account, url)
|
||||||
cache_key = [account.id, url].join(':')
|
cache_key = [account.id, url].join(':')
|
||||||
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)
|
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)
|
||||||
|
|
||||||
@@ -134,11 +180,15 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_blob_by_checksum(checksum, account)
|
def find_blob_by_checksum(checksum, account)
|
||||||
ActiveStorage::Blob
|
blob = ActiveStorage::Blob.find_by(checksum:)
|
||||||
.joins('JOIN active_storage_attachments ON active_storage_attachments.blob_id = active_storage_blobs.id')
|
|
||||||
.where(active_storage_attachments: { record_id: account.submitters.select(:id),
|
return unless blob
|
||||||
record_type: 'Submitter' })
|
|
||||||
.find_by(checksum:)
|
return blob unless blob.attachments.exists?
|
||||||
|
|
||||||
|
return blob if account.submitters.exists?(id: blob.attachments.where(record_type: 'Submitter').select(:record_id))
|
||||||
|
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def conn
|
def conn
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ module Submitters
|
|||||||
serialize_params = {
|
serialize_params = {
|
||||||
include: {},
|
include: {},
|
||||||
only: %i[id slug uuid name email phone completed_at external_id
|
only: %i[id slug uuid name email phone completed_at external_id
|
||||||
metadata opened_at sent_at created_at updated_at],
|
submission_id metadata opened_at sent_at created_at updated_at],
|
||||||
methods: %i[status application_key]
|
methods: %i[status application_key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,15 +16,24 @@ module Submitters
|
|||||||
submitter_name = (submitter.submission.template_submitters ||
|
submitter_name = (submitter.submission.template_submitters ||
|
||||||
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
|
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
|
||||||
|
|
||||||
submitter.as_json(methods: %i[application_key],
|
submitter.as_json(methods: %i[status application_key],
|
||||||
include: [template: { only: %i[id name external_id created_at updated_at] }])
|
only: %i[id submission_id email phone name ua ip sent_at opened_at
|
||||||
|
completed_at created_at updated_at external_id metadata])
|
||||||
.except('uuid', 'values', 'slug')
|
.except('uuid', 'values', 'slug')
|
||||||
.merge('values' => values,
|
.merge('role' => submitter_name,
|
||||||
|
'preferences' => submitter.preferences.except('default_values'),
|
||||||
|
'values' => values,
|
||||||
'documents' => documents,
|
'documents' => documents,
|
||||||
'audit_log_url' => submitter.submission.audit_log_url,
|
'audit_log_url' => submitter.submission.audit_log_url,
|
||||||
'submission_url' => r.submissions_preview_url(submitter.submission.slug,
|
'submission_url' => r.submissions_preview_url(submitter.submission.slug,
|
||||||
**Docuseal.default_url_options),
|
**Docuseal.default_url_options),
|
||||||
'role' => submitter_name)
|
'template' => submitter.template.as_json(only: %i[id name external_id created_at
|
||||||
|
updated_at]),
|
||||||
|
'submission' => {
|
||||||
|
**submitter.submission.slice(:id, :audit_log_url, :created_at),
|
||||||
|
status: submitter.submission.submitters.all?(&:completed_at?) ? 'completed' : 'pending',
|
||||||
|
url: r.submissions_preview_url(submitter.submission.slug, **Docuseal.default_url_options)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_values_array(submitter)
|
def build_values_array(submitter)
|
||||||
@@ -67,7 +76,7 @@ module Submitters
|
|||||||
def rails_storage_proxy_url(attachment)
|
def rails_storage_proxy_url(attachment)
|
||||||
return if attachment.blank?
|
return if attachment.blank?
|
||||||
|
|
||||||
r.rails_storage_proxy_url(attachment, **Docuseal.default_url_options)
|
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
def r
|
def r
|
||||||
|
|||||||
@@ -98,8 +98,6 @@ module Templates
|
|||||||
record: attachment
|
record: attachment
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
io
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
+1
-1
@@ -42,7 +42,7 @@ module TimeUtils
|
|||||||
|
|
||||||
I18n.l(date, format: i18n_format, locale:)
|
I18n.l(date, format: i18n_format, locale:)
|
||||||
rescue Date::Error => e
|
rescue Date::Error => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
string
|
string
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ RSpec.describe 'Submission Preview' do
|
|||||||
let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
|
let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
visit submissions_preview_path(slug: submission.slug)
|
visit submissions_preview_path(slug: submission.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user