2023-08-04 00:40:13 +03:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
|
|
module Api
|
|
|
|
|
class SubmissionsController < ApiBaseController
|
2026-06-28 10:52:54 +03:00
|
|
|
SUBMISSION_COLUMNS = %i[id name slug source submitters_order expire_at created_at updated_at
|
|
|
|
|
archived_at variables template_id template_submitters created_by_user_id].freeze
|
|
|
|
|
TEMPLATE_COLUMNS = %i[id name external_id created_at updated_at folder_id submitters].freeze
|
|
|
|
|
|
2023-10-01 01:57:47 +03:00
|
|
|
load_and_authorize_resource :template, only: :create
|
2023-11-16 01:55:57 +02:00
|
|
|
load_and_authorize_resource :submission, only: %i[show index destroy]
|
2023-09-17 00:45:57 +03:00
|
|
|
|
2023-10-01 01:57:47 +03:00
|
|
|
before_action only: :create do
|
2023-09-17 00:45:57 +03:00
|
|
|
authorize!(:create, Submission)
|
|
|
|
|
end
|
2023-08-04 00:40:13 +03:00
|
|
|
|
2023-10-01 01:57:47 +03:00
|
|
|
def index
|
2025-06-05 13:16:55 +03:00
|
|
|
submissions = Submissions.search(current_user, @submissions, params[:q])
|
2025-04-15 13:22:02 +03:00
|
|
|
submissions = filter_submissions(submissions, params)
|
2025-01-03 20:27:55 +02:00
|
|
|
|
2026-06-29 10:22:58 +03:00
|
|
|
with_fields = params[:include].to_s.include?('fields') || params[:include].to_s.include?('combined_document_url')
|
2026-06-28 10:52:54 +03:00
|
|
|
|
|
|
|
|
submissions = paginate(
|
2026-06-29 10:22:58 +03:00
|
|
|
submissions.select(with_fields ? nil : SUBMISSION_COLUMNS)
|
2026-06-28 10:52:54 +03:00
|
|
|
.preload(:created_by_user, :submitters, combined_document_attachment: :blob,
|
|
|
|
|
audit_trail_attachment: :blob)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ActiveRecord::Associations::Preloader.new(
|
|
|
|
|
records: submissions,
|
|
|
|
|
associations: :template,
|
2026-06-29 10:22:58 +03:00
|
|
|
scope: with_fields ? nil : Template.select(TEMPLATE_COLUMNS)
|
2026-06-28 10:52:54 +03:00
|
|
|
).call
|
|
|
|
|
|
|
|
|
|
ActiveRecord::Associations::Preloader.new(records: submissions.filter_map(&:template),
|
|
|
|
|
associations: { folder: :parent_folder }).call
|
2023-10-01 01:57:47 +03:00
|
|
|
|
2025-08-31 16:40:18 +03:00
|
|
|
expires_at = Accounts.link_expires_at(current_account)
|
|
|
|
|
|
2023-10-01 01:57:47 +03:00
|
|
|
render json: {
|
2024-08-27 01:50:26 +03:00
|
|
|
data: submissions.map do |s|
|
|
|
|
|
Submissions::SerializeForApi.call(s, s.submitters, params,
|
2025-08-31 16:40:18 +03:00
|
|
|
with_events: false, with_documents: false, with_values: false, expires_at:)
|
2024-08-27 01:50:26 +03:00
|
|
|
end,
|
2023-10-01 01:57:47 +03:00
|
|
|
pagination: {
|
|
|
|
|
count: submissions.size,
|
|
|
|
|
next: submissions.last&.id,
|
|
|
|
|
prev: submissions.first&.id
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def show
|
2023-12-23 22:24:57 +02:00
|
|
|
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
2023-10-01 01:57:47 +03:00
|
|
|
|
2024-02-10 19:38:03 +02:00
|
|
|
submitters.each do |submitter|
|
|
|
|
|
if submitter.completed_at? && submitter.documents_attachments.blank?
|
|
|
|
|
submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
|
|
|
|
|
end
|
|
|
|
|
end
|
2023-12-23 22:24:57 +02:00
|
|
|
|
2024-02-10 19:38:03 +02:00
|
|
|
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
2025-09-15 09:10:03 +03:00
|
|
|
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
|
2023-12-23 22:24:57 +02:00
|
|
|
end
|
2023-10-01 01:57:47 +03:00
|
|
|
|
2024-07-01 12:06:05 +03:00
|
|
|
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
2023-10-01 01:57:47 +03:00
|
|
|
end
|
|
|
|
|
|
2023-09-17 00:45:57 +03:00
|
|
|
def create
|
2024-04-27 21:35:20 +03:00
|
|
|
Params::SubmissionCreateValidator.call(params)
|
2024-01-19 03:03:45 +02:00
|
|
|
|
2025-08-14 09:25:35 +03:00
|
|
|
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
2023-12-28 17:39:09 +02:00
|
|
|
|
2026-06-09 19:34:58 +03:00
|
|
|
if @template.archived_at?
|
|
|
|
|
Rollbar.warning("Archived template submission: #{@template.id}") if defined?(Rollbar)
|
|
|
|
|
|
|
|
|
|
return render json: { error: 'Template has been archived' }, status: :unprocessable_content
|
|
|
|
|
end
|
|
|
|
|
|
2024-01-14 00:49:07 +02:00
|
|
|
if @template.fields.blank?
|
2024-02-16 19:44:59 +02:00
|
|
|
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
2024-01-14 00:49:07 +02:00
|
|
|
|
2025-08-14 09:25:35 +03:00
|
|
|
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
|
2024-01-14 00:49:07 +02:00
|
|
|
end
|
|
|
|
|
|
2023-11-23 20:03:52 +02:00
|
|
|
params[:send_email] = true unless params.key?(:send_email)
|
|
|
|
|
params[:send_sms] = false unless params.key?(:send_sms)
|
2023-08-04 00:40:13 +03:00
|
|
|
|
2023-11-23 20:03:52 +02:00
|
|
|
submissions = create_submissions(@template, params)
|
2023-09-23 22:00:23 +03:00
|
|
|
|
2025-07-04 21:11:08 +03:00
|
|
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
2024-05-17 13:28:10 +03:00
|
|
|
|
2023-11-23 20:03:52 +02:00
|
|
|
Submissions.send_signature_requests(submissions)
|
2023-08-04 00:40:13 +03:00
|
|
|
|
2024-02-10 19:38:03 +02:00
|
|
|
submissions.each do |submission|
|
2024-10-26 19:42:16 +03:00
|
|
|
submission.submitters.each do |submitter|
|
2024-12-09 01:06:57 +02:00
|
|
|
next unless submitter.completed_at?
|
|
|
|
|
|
|
|
|
|
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
|
2024-02-10 19:38:03 +02:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2025-06-05 13:16:55 +03:00
|
|
|
SearchEntries.enqueue_reindex(submissions)
|
|
|
|
|
|
2024-07-18 23:59:27 +03:00
|
|
|
render json: build_create_json(submissions)
|
2024-10-22 22:26:08 +03:00
|
|
|
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
|
|
|
|
DownloadUtils::UnableToDownload => e
|
2024-02-16 19:44:59 +02:00
|
|
|
Rollbar.warning(e) if defined?(Rollbar)
|
2024-02-13 22:02:15 +02:00
|
|
|
|
2025-08-14 09:25:35 +03:00
|
|
|
render json: { error: e.message }, status: :unprocessable_content
|
2023-08-04 00:40:13 +03:00
|
|
|
end
|
|
|
|
|
|
2023-10-01 01:57:47 +03:00
|
|
|
def destroy
|
2024-12-13 13:31:54 +02:00
|
|
|
if params[:permanently].in?(['true', true])
|
2024-06-18 16:45:07 +03:00
|
|
|
@submission.destroy!
|
|
|
|
|
else
|
|
|
|
|
@submission.update!(archived_at: Time.current)
|
2023-10-01 01:57:47 +03:00
|
|
|
|
2025-07-04 21:11:08 +03:00
|
|
|
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
2024-06-18 16:45:07 +03:00
|
|
|
end
|
2024-04-09 18:11:58 +03:00
|
|
|
|
2024-06-18 16:45:07 +03:00
|
|
|
render json: @submission.as_json(only: %i[id archived_at])
|
2023-10-01 01:57:47 +03:00
|
|
|
end
|
|
|
|
|
|
2023-08-04 00:40:13 +03:00
|
|
|
private
|
|
|
|
|
|
2025-04-15 13:22:02 +03:00
|
|
|
def filter_submissions(submissions, params)
|
|
|
|
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
|
|
|
|
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
|
|
|
|
|
|
|
|
|
if params[:template_folder].present?
|
2025-07-20 08:28:19 +03:00
|
|
|
folders =
|
|
|
|
|
TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder])
|
2025-06-16 16:48:20 +03:00
|
|
|
|
2025-07-20 08:28:19 +03:00
|
|
|
submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) })
|
2025-04-15 13:22:02 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if params.key?(:archived)
|
|
|
|
|
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
Submissions::Filter.call(submissions, current_user, params)
|
|
|
|
|
end
|
|
|
|
|
|
2024-07-18 23:59:27 +03:00
|
|
|
def build_create_json(submissions)
|
|
|
|
|
json = submissions.flat_map do |submission|
|
|
|
|
|
submission.submitters.map do |s|
|
|
|
|
|
Submitters::SerializeForApi.call(s, with_documents: false, with_urls: true, params:)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2025-01-02 13:18:17 +02:00
|
|
|
if request.path.ends_with?('/init')
|
|
|
|
|
json =
|
|
|
|
|
if submissions.size == 1
|
|
|
|
|
{
|
|
|
|
|
id: submissions.first.id,
|
|
|
|
|
submitters: json,
|
|
|
|
|
expire_at: submissions.first.expire_at,
|
|
|
|
|
created_at: submissions.first.created_at
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{ submitters: json }
|
|
|
|
|
end
|
|
|
|
|
end
|
2024-07-18 23:59:27 +03:00
|
|
|
|
|
|
|
|
json
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-23 20:03:52 +02:00
|
|
|
def create_submissions(template, params)
|
|
|
|
|
is_send_email = !params[:send_email].in?(['false', false])
|
|
|
|
|
|
2024-01-28 14:31:16 +02:00
|
|
|
if (emails = (params[:emails] || params[:email]).presence) &&
|
2025-10-04 18:33:37 +03:00
|
|
|
params[:submission].blank? && params[:submitters].blank?
|
2023-11-23 20:03:52 +02:00
|
|
|
Submissions.create_from_emails(template:,
|
|
|
|
|
user: current_user,
|
|
|
|
|
source: :api,
|
|
|
|
|
mark_as_sent: is_send_email,
|
|
|
|
|
emails:,
|
|
|
|
|
params:)
|
|
|
|
|
else
|
2023-12-17 18:39:10 +02:00
|
|
|
submissions_attrs, attachments =
|
2026-04-10 15:20:35 +03:00
|
|
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template, purpose: :api)
|
2023-11-23 20:03:52 +02:00
|
|
|
|
|
|
|
|
submissions = Submissions.create_from_submitters(
|
|
|
|
|
template:,
|
|
|
|
|
user: current_user,
|
|
|
|
|
source: :api,
|
|
|
|
|
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
|
|
|
|
|
submissions_attrs:,
|
|
|
|
|
params:
|
|
|
|
|
)
|
|
|
|
|
|
2024-02-10 19:38:03 +02:00
|
|
|
submitters = submissions.flat_map(&:submitters)
|
|
|
|
|
|
|
|
|
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
|
|
|
|
|
|
|
|
|
submitters.each do |submitter|
|
2026-02-20 12:27:39 +02:00
|
|
|
if submitter.completed_at?
|
|
|
|
|
Submitters::SubmitValues.maybe_invite_via_field(submitter, request)
|
|
|
|
|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request)
|
|
|
|
|
end
|
2024-02-10 19:38:03 +02:00
|
|
|
end
|
2023-11-23 20:03:52 +02:00
|
|
|
|
|
|
|
|
submissions
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2023-08-04 00:40:13 +03:00
|
|
|
def submissions_params
|
2024-01-28 14:31:16 +02:00
|
|
|
permitted_attrs = [
|
2024-04-13 15:33:05 +03:00
|
|
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
2025-12-09 11:46:57 +02:00
|
|
|
:require_phone_2fa, :require_email_2fa, :expire_at, :name,
|
2024-01-28 14:31:16 +02:00
|
|
|
{
|
2025-09-12 12:17:26 +03:00
|
|
|
variables: {},
|
2024-01-28 14:31:16 +02:00
|
|
|
message: %i[subject body],
|
|
|
|
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
2024-04-13 15:33:05 +03:00
|
|
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
2026-01-29 12:57:26 +02:00
|
|
|
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
2025-03-16 21:56:30 +02:00
|
|
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
2024-04-27 21:35:09 +03:00
|
|
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
2025-06-10 11:26:12 +03:00
|
|
|
:readonly, :required, :validation_pattern, :invalid_message,
|
2025-08-20 18:19:23 +03:00
|
|
|
{ default_value: [], value: [], preferences: {}, validation: {} }] }]]
|
2024-01-28 14:31:16 +02:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if params.key?(:submitters)
|
|
|
|
|
params.permit(*permitted_attrs)
|
|
|
|
|
else
|
|
|
|
|
key = params.key?(:submission) ? :submission : :submissions
|
|
|
|
|
|
|
|
|
|
params.permit(
|
2024-12-24 00:29:25 +02:00
|
|
|
{ key => [permitted_attrs] }, { key => permitted_attrs }
|
2024-01-28 14:31:16 +02:00
|
|
|
).fetch(key, [])
|
|
|
|
|
end
|
2023-08-11 18:32:33 +03:00
|
|
|
end
|
2023-08-04 00:40:13 +03:00
|
|
|
end
|
|
|
|
|
end
|