store multiple submitter results

pull/105/head
Alex Turchyn 2 years ago
parent 279409ad41
commit 28429c1167

@ -39,6 +39,12 @@ Metrics/PerceivedComplexity:
Metrics/AbcSize:
Max: 35
Metrics/ModuleLength:
Max: 500
Metrics/ClassLength:
Max: 500
RSpec/NestedGroups:
Max: 6

@ -5,14 +5,14 @@ module Api
skip_before_action :authenticate_user!
def create
submission = Submission.find_by!(slug: params[:submission_slug]) unless current_account
submitter = Submitter.find_by!(slug: params[:submitter_slug]) unless current_account
blob = ActiveStorage::Blob.find_signed(params[:blob_signed_id])
attachment = ActiveStorage::Attachment.create!(
blob:,
name: params[:name],
record: submission || current_account
record: submitter || current_account
)
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])

@ -9,14 +9,15 @@ class SendSubmissionEmailController < ApplicationController
def success; end
def create
@submission =
@submitter =
if params[:template_slug]
Submission.joins(:template).find_by!(email: params[:email], template: { slug: params[:template_slug] })
Submitter.joins(submission: :template).find_by!(email: params[:email],
template: { slug: params[:template_slug] })
else
Submission.find_by!(slug: params[:submission_slug])
Submitter.find_by!(slug: params[:submitter_slug])
end
SubmissionMailer.copy_to_submitter(@submission).deliver_later!
SubmitterMailer.copy_to_submitter(@submitter).deliver_later!
respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path }

@ -8,25 +8,27 @@ class StartFormController < ApplicationController
before_action :load_template
def show
@submission = @template.submissions.new
@submitter = @template.submissions.new.submitters.new(uuid: @template.submitters.first['uuid'])
end
def update
@submission = @template.submissions.find_or_initialize_by(
deleted_at: nil, **submission_params
)
@submitter = Submitter.where(submission: @template.submissions.where(submission: { deleted_at: nil }))
.find_or_initialize_by(**submitter_params)
if @submission.completed_at?
if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submission_params[:email])
else
@submission.assign_attributes(
@submitter.assign_attributes(
uuid: @template.submitters.first['uuid'],
opened_at: Time.current,
ip: request.remote_ip,
ua: request.user_agent
)
if @submission.save
redirect_to submit_form_path(@submission.slug)
@submitter.build_submission(template: @template)
if @submitter.save
redirect_to submit_form_path(@submitter.slug)
else
render :show
end
@ -34,13 +36,13 @@ class StartFormController < ApplicationController
end
def completed
@submission = @template.submissions.find_by(email: params[:email])
@submitter = Submitter.where(submission: @template.submitters).find_by(email: params[:email])
end
private
def submission_params
params.require(:submission).permit(:email)
def submitter_params
params.require(:submitter).permit(:email)
end
def load_template

@ -17,18 +17,18 @@ class SubmissionsController < ApplicationController
def new; end
def create
emails = params[:emails].to_s.scan(User::EMAIL_REGEXP)
submissions =
emails.map do |email|
submission = @template.submissions.create!(email:, sent_at: params[:send_email] == '1' ? Time.current : nil)
if params[:send_email] == '1'
SubmissionMailer.invitation_email(submission, message: params[:message]).deliver_later!
end
if params[:emails].present?
create_submissions_from_emails
else
create_submissions_from_submitters
end
submission
if params[:send_email] == '1'
submissions.flat_map(&:submitters).each do |submitter|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
end
end
redirect_to template_submissions_path(@template), notice: "#{submissions.size} recepients added"
end
@ -44,6 +44,34 @@ class SubmissionsController < ApplicationController
private
def create_submissions_from_emails
emails = params[:emails].to_s.scan(User::EMAIL_REGEXP)
emails.map do |email|
submission = @template.submissions.new
submission.submitters.new(email:, uuid: @template.submitters.first['uuid'],
sent_at: params[:send_email] == '1' ? Time.current : nil)
submission.tap(&:save!)
end
end
def create_submissions_from_submitters
submissions_params.map do |attrs|
submission = @template.submissions.new
attrs[:submitters].each do |submitter_attrs|
submission.submitters.new(**submitter_attrs, sent_at: params[:send_email] == '1' ? Time.current : nil)
end
submission.tap(&:save!)
end
end
def submissions_params
params.require(:submission).map { |param| param.permit(submitters: [%i[uuid email]]) }
end
def load_template
@template = current_account.templates.find(params[:template_id])
end

@ -6,16 +6,16 @@ class SubmissionsDebugController < ApplicationController
skip_before_action :authenticate_user!
def index
@submission = Submission.preload({ attachments_attachments: :blob },
template: { documents_attachments: :blob })
.find_by(slug: params[:submission_slug])
@submitter = Submitter.preload({ attachments_attachments: :blob },
submission: { template: { documents_attachments: :blob } })
.find_by(slug: params[:submitter_slug])
respond_to do |f|
f.html do
render 'submit_template/show'
end
f.pdf do
Submissions::GenerateResultAttachments.call(@submission)
Submissions::GenerateResultAttachments.call(@submitter.submission)
send_data ActiveStorage::Attachment.where(name: :documents).last.download,
filename: 'debug.pdf',

@ -4,10 +4,10 @@ class SubmissionsDownloadController < ApplicationController
skip_before_action :authenticate_user!
def index
submission = Submission.find_by(slug: params[:submission_slug])
submitter = Submitter.find_by(slug: params[:submitter_slug])
Submissions::GenerateResultAttachments.call(submission)
Submissions::GenerateResultAttachments.call(submitter)
redirect_to submission.archive.url, allow_other_host: true
redirect_to submitter.archive.url, allow_other_host: true
end
end

@ -6,24 +6,25 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user!
def show
@submission = Submission.preload(template: { documents_attachments: { preview_images_attachments: :blob } })
.find_by!(slug: params[:slug])
@submitter =
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } })
.find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submission.slug) if @submission.completed_at?
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
end
def update
submission = Submission.find_by!(slug: params[:slug])
submission.values.merge!(normalized_values)
submission.completed_at = Time.current if params[:completed] == 'true'
submitter = Submitter.find_by!(slug: params[:slug])
submitter.values.merge!(normalized_values)
submitter.completed_at = Time.current if params[:completed] == 'true'
submission.save
submitter.save
head :ok
end
def completed
@submission = Submission.find_by!(slug: params[:submit_form_slug])
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
end
private

@ -62,7 +62,7 @@ export default actionable(targetable(class extends HTMLElement {
body: JSON.stringify({
name: this.dataset.name,
blob_signed_id: blob.signed_id,
submission_slug: this.dataset.submissionSlug
submitter_slug: this.dataset.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(resp => resp.json()).then((data) => {

@ -7,7 +7,8 @@ window.customElements.define('submission-form', class extends HTMLElement {
this.appElem = document.createElement('div')
this.app = createApp(Form, {
submissionSlug: this.dataset.submissionSlug,
submitterSlug: this.dataset.submitterSlug,
submitterUuid: this.dataset.submitterUuid,
authenticityToken: this.dataset.authenticityToken,
values: reactive(JSON.parse(this.dataset.values)),
attachments: reactive(JSON.parse(this.dataset.attachments)),

@ -33,7 +33,7 @@
</template>
<FileDropzone
:message="'Attachments'"
:submission-slug="submissionSlug"
:submitter-slug="submitterSlug"
@upload="onUpload"
/>
</div>
@ -52,7 +52,7 @@ export default {
type: Object,
required: true
},
submissionSlug: {
submitterSlug: {
type: String,
required: true
},

@ -26,7 +26,7 @@
export default {
name: 'FormCompleted',
props: {
submissionSlug: {
submitterSlug: {
type: String,
required: true
}
@ -41,7 +41,7 @@ export default {
sendCopyToEmail () {
this.isSendingCopy = true
fetch(`/send_submission_email.json?submission_slug=${this.submissionSlug}`, {
fetch(`/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
method: 'POST'
}).finally(() => {
this.isSendingCopy = false
@ -50,7 +50,7 @@ export default {
download () {
this.isDownloading = true
fetch(`/submissions/${this.submissionSlug}/download`).then(async (response) => {
fetch(`/submitters/${this.submitterSlug}/download`).then(async (response) => {
const blob = new Blob([await response.text()], { type: `${response.headers.get('content-type')};charset=utf-8;` })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')

@ -33,7 +33,7 @@ export default {
type: String,
required: true
},
submissionSlug: {
submitterSlug: {
type: String,
required: true
},
@ -97,7 +97,7 @@ export default {
body: JSON.stringify({
name: 'attachments',
blob_signed_id: blob.signed_id,
submission_slug: this.submissionSlug
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(resp => resp.json()).then((data) => {

@ -1,14 +1,14 @@
<template>
<FieldAreas
ref="areas"
:fields="fields"
:fields="submitterFields"
:values="values"
:attachments-index="attachmentsIndex"
@focus-field="goToField"
/>
<button
v-if="currentStep !== 0"
@click="goToField(fields[currentStep - 1], true)"
@click="goToField(submitterFields[currentStep - 1], true)"
>
Back
</button>
@ -26,7 +26,7 @@
:value="authenticityToken"
>
<input
v-if="currentStep === fields.length - 1"
v-if="currentStep === submitterFields.length - 1"
type="hidden"
name="completed"
value="true"
@ -117,7 +117,7 @@
v-model="values[currentField.uuid]"
:field="currentField"
:attachments-index="attachmentsIndex"
:submission-slug="submissionSlug"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
<SignatureStep
@ -126,7 +126,7 @@
v-model="values[currentField.uuid]"
:field="currentField"
:attachments-index="attachmentsIndex"
:submission-slug="submissionSlug"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
<AttachmentStep
@ -134,7 +134,7 @@
v-model="values[currentField.uuid]"
:field="currentField"
:attachments-index="attachmentsIndex"
:submission-slug="submissionSlug"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
</div>
@ -151,7 +151,7 @@
</form>
<FormCompleted
v-else
:submission-slug="submissionSlug"
:submitter-slug="submitterSlug"
/>
</template>
@ -174,7 +174,11 @@ export default {
FormCompleted
},
props: {
submissionSlug: {
submitterSlug: {
type: String,
required: true
},
submitterUuid: {
type: String,
required: true
},
@ -207,7 +211,10 @@ export default {
},
computed: {
currentField () {
return this.fields[this.currentStep]
return this.submitterFields[this.currentStep]
},
submitterFields () {
return this.fields.filter((f) => f.submitter_uuid === this.submitterUuid)
},
attachmentsIndex () {
return this.attachments.reduce((acc, a) => {
@ -217,18 +224,18 @@ export default {
}, {})
},
submitPath () {
return `/l/${this.submissionSlug}`
return `/s/${this.submitterSlug}`
}
},
mounted () {
this.currentStep = Math.min(
this.fields.indexOf([...this.fields].reverse().find((field) => !!this.values[field.uuid])) + 1,
this.fields.length - 1
this.submitterFields.indexOf([...this.submitterFields].reverse().find((field) => !!this.values[field.uuid])) + 1,
this.submitterFields.length - 1
)
},
methods: {
goToField (field, scrollToArea = false) {
this.currentStep = this.fields.indexOf(field)
this.currentStep = this.submitterFields.indexOf(field)
this.$nextTick(() => {
if (scrollToArea) {
@ -251,10 +258,10 @@ export default {
method: 'POST',
body: new FormData(this.$refs.form)
}).then(response => {
const nextField = this.fields[this.currentStep + 1]
const nextField = this.submitterFields[this.currentStep + 1]
if (nextField) {
this.goToField(this.fields[this.currentStep + 1], true)
this.goToField(this.submitterFields[this.currentStep + 1], true)
} else {
this.isCompleted = true
}

@ -18,7 +18,7 @@
>
<FileDropzone
:message="'Image'"
:submission-slug="submissionSlug"
:submitter-slug="submitterSlug"
:accept="'image/*'"
@upload="onImageUpload"
/>
@ -37,7 +37,7 @@ export default {
type: Object,
required: true
},
submissionSlug: {
submitterSlug: {
type: String,
required: true
},

@ -39,7 +39,7 @@ export default {
type: Object,
required: true
},
submissionSlug: {
submitterSlug: {
type: String,
required: true
},
@ -81,7 +81,7 @@ export default {
fetch('/api/attachments', {
method: 'POST',
body: JSON.stringify({
submission_slug: this.submissionSlug,
submitter_slug: this.submitterSlug,
blob_signed_id: data.signed_id,
name: 'attachments'
}),

@ -1,19 +0,0 @@
# frozen_string_literal: true
class SubmissionMailer < ApplicationMailer
DEFAULT_MESSAGE = "You've been invited to submit documents."
def invitation_email(submission, message: DEFAULT_MESSAGE)
@submission = submission
@message = message
mail(to: @submission.email,
subject: 'You have been invited to submit forms')
end
def copy_to_submitter(submission)
@submission = submission
mail(to: submission.email, subject: 'Here is your copy')
end
end

@ -0,0 +1,19 @@
# frozen_string_literal: true
class SubmitterMailer < ApplicationMailer
DEFAULT_MESSAGE = "You've been invited to submit documents."
def invitation_email(submitter, message: DEFAULT_MESSAGE)
@submitter = submitter
@message = message
mail(to: @submitter.email,
subject: 'You have been invited to submit forms')
end
def copy_to_submitter(submitter)
@submitter = submitter
mail(to: submitter.email, subject: 'Here is your copy')
end
end

@ -4,24 +4,14 @@
#
# Table name: submissions
#
# id :bigint not null, primary key
# completed_at :datetime
# deleted_at :datetime
# email :string not null
# ip :string
# opened_at :datetime
# sent_at :datetime
# slug :string not null
# ua :string
# values :string not null
# created_at :datetime not null
# updated_at :datetime not null
# template_id :bigint not null
# id :bigint not null, primary key
# deleted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# template_id :bigint not null
#
# Indexes
#
# index_submissions_on_email (email)
# index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id)
#
# Foreign Keys
@ -31,27 +21,7 @@
class Submission < ApplicationRecord
belongs_to :template
attribute :values, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(8) }
serialize :values, JSON
has_one_attached :archive
has_many_attached :documents
has_many_attached :attachments
has_many :submitters, dependent: :destroy
scope :active, -> { where(deleted_at: nil) }
def status
if completed_at?
'completed'
elsif opened_at?
'opened'
elsif sent_at?
'sent'
else
'awaiting'
end
end
end

@ -0,0 +1,55 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: submitters
#
# id :bigint not null, primary key
# completed_at :datetime
# email :string not null
# ip :string
# opened_at :datetime
# sent_at :datetime
# slug :string not null
# ua :string
# uuid :string not null
# values :string not null
# created_at :datetime not null
# updated_at :datetime not null
# submission_id :bigint not null
#
# Indexes
#
# index_submitters_on_email (email)
# index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id)
#
# Foreign Keys
#
# fk_rails_... (submission_id => submissions.id)
#
class Submitter < ApplicationRecord
belongs_to :submission
attribute :values, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(8) }
serialize :values, JSON
has_one_attached :archive
has_many_attached :documents
has_many_attached :attachments
def status
if completed_at?
'completed'
elsif opened_at?
'opened'
elsif sent_at?
'sent'
else
'awaiting'
end
end
end

@ -1,5 +1,5 @@
You have been invited to submit template <%= @template.name %>
<%= form_for @submission, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put do |f| %>
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put do |f| %>
Provide youe email to start
<div>
<%= f.label :email %>

@ -1,3 +0,0 @@
<p>Hi</a>
<%= @submission.values %>
<%= link_to 'Download', submission_download_index_url(@submission.slug) %>

@ -4,16 +4,18 @@
<%= @template.name %>
<%= link_to 'Edit', template_path(@template), class: 'btn btn-outline btn-sm' %>
</h2>
<div class="join w-full">
<buttun class="btn bg-neutral btn-disabled text-white join-item">
Share link
</buttun>
<input id="share-link-input" autocomplete="off" type="text" class="input input-bordered w-full join-item" value="<%= start_form_url(slug: @template.slug) %>" disabled>
<clipboard-copy class="btn btn-neutral btn-square join-item text-white font-bold swap swap-active" for="share-link-input">
<%= svg_icon('clipboard', class: 'w-6 h-6 swap-on text-white') %>
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 swap-off text-white') %>
</clipboard-copy>
</div>
<% if @template.submitters.size == 1 %>
<div class="join w-full">
<buttun class="btn bg-neutral btn-disabled text-white join-item">
Share link
</buttun>
<input id="share-link-input" autocomplete="off" type="text" class="input input-bordered w-full join-item" value="<%= start_form_url(slug: @template.slug) %>" disabled>
<clipboard-copy class="btn btn-neutral btn-square join-item text-white font-bold swap swap-active" for="share-link-input">
<%= svg_icon('clipboard', class: 'w-6 h-6 swap-on text-white') %>
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 swap-off text-white') %>
</clipboard-copy>
</div>
<% end %>
</div>
</div>
<div class="flex justify-between mb-4">
@ -42,15 +44,22 @@
<% @submissions.each do |submission| %>
<tr>
<td>
<%= submission.email %>
<% submission.submitters.each do |submitter| %>
<%= submitter.email %>
<% end %>
</td>
<td>
<span class="badge badge-info badge-outline">
<%= submission.status.humanize %>
</span>
<% submission.submitters.each do |submitter| %>
<div>
<span class="badge badge-info badge-outline">
<%= submitter.status %>
</span>
<%= link_to 'Copy', submit_form_url(slug: submitter.slug), title: 'Copy', class: 'btn btn-outline btn-xs' %>
</div>
<% end %>
</td>
<td class="flex items-center space-x-2 justify-end">
<%= link_to 'View', submission_path(@template), title: 'View', class: 'btn btn-outline btn-xs' %>
<%= link_to 'View', submission_path(submission), title: 'View', class: 'btn btn-outline btn-xs' %>
<%= button_to 'Remove', submission_path(submission), class: 'btn btn-outline btn-error btn-xs', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
</td>
</tr>

@ -1,9 +1,41 @@
<%= render 'shared/turbo_modal', title: 'New Recepients' do %>
<%= form_for '', url: template_submissions_path(@template), html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
<div class="form-control">
<%= f.label :emails, class: 'label' %>
<%= f.text_area :emails, required: true, class: 'base-textarea' %>
</div>
<% if @template.submitters.size == 1 %>
<div class="form-control">
<%= f.label :emails, class: 'label' %>
<%= f.text_area :emails, required: true, class: 'base-textarea' %>
</div>
<% else %>
<table class="">
<thead>
<tr>
<% @template.submitters.each do |item| %>
<th class="w-1/2 text-left">
<%= item['name'] %>
</th>
<% end %>
<th>
</th>
</tr>
</thead>
<tbody>
<tr>
<% @template.submitters.each do |item| %>
<td>
<input type="hidden" name="submission[][submitters][][uuid]" value="<%= item['uuid'] %>" >
<input type="email" name="submission[][submitters][][email]" value="<%= item['email'] %>" required>
</td>
<% end %>
<td>
&times;
</td>
</tr>
</tbody>
</table>
<button>
+ Add new
</button>
<% end %>
<div class="form-control">
<%= f.label :send_email, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, class: 'base-checkbox', onchange: "message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
@ -12,23 +44,18 @@
</div>
<div id="message_field" class="card card-compact bg-base-200 hidden">
<div class="card-body">
<div class="form-control">
<label class="label">
<span class="label-text">Hi There,</span>
</label>
<%= f.text_area :message, value: SubmissionMailer::DEFAULT_MESSAGE, required: true, class: 'base-textarea !rounded-lg' %>
<label class="label">
<span class="label-text">
Thanks,
</br>
<%= current_account.name %>
</span>
</label>
<div class="form-control space-y-2">
<span class="label-text">Hi there,</span>
<%= f.text_area :message, value: SubmitterMailer::DEFAULT_MESSAGE, required: true, class: 'base-textarea !rounded-lg' %>
<span class="label-text">
Thanks,
</br>
<%= current_account.name %>
</div>
</div>
</div>
</div>
<div class="form-control">
<%= f.button button_title(title: 'Confirm', disabled_with: 'Processing'), class: 'base-button' %>
</div>
<% end %>
<div class="form-control">
<%= f.button button_title(title: 'Confirm', disabled_with: 'Processing'), class: 'base-button' %>
</div>
<% end %>
<% end %>

@ -12,18 +12,6 @@
<%= link_to @submission.template.name, template_submissions_path(@submission.template), class: 'link link-hover' %>
</dd>
</div>
<div class="py-6 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium leading-6 text-gray-900">URL</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
<%= link_to submit_form_url(slug: @submission.slug), submit_form_url(slug: @submission.slug), class: 'link link-hover' %>
</dd>
</div>
<div class="py-6 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium leading-6 text-gray-900">Email address</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
<%= mail_to @submission.email %>
</dd>
</div>
<%- if @submission.completed_at.present? %>
<div class="py-6 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium leading-6 text-gray-900">Completed at</dt>
@ -40,35 +28,41 @@
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
<% if ['image', 'signature'].include?(field['type']) %>
<ul role="list" class="divide-y divide-gray-100 rounded-md border border-gray-200">
<% Array.wrap(@submission.values[field['uuid']]).each do |uuid| %>
<li class="py-4 pl-4 pr-5 text-sm leading-6">
<div class="flex w-0 flex-1 items-center">
<%= image_tag ActiveStorage::Attachment.find_by(uuid:).url %>
</div>
</li>
<% @submission.submitters.each do |submitter| %>
<% Array.wrap(submitter.values[field['uuid']]).each do |uuid| %>
<li class="py-4 pl-4 pr-5 text-sm leading-6">
<div class="flex w-0 flex-1 items-center">
<%= image_tag ActiveStorage::Attachment.find_by(uuid:).url %>
</div>
</li>
<% end %>
<% end %>
</ul>
<% elsif ['attachment'].include?(field['type']) %>
<ul role="list" class="divide-y divide-gray-100 rounded-md border border-gray-200">
<% Array.wrap(@submission.values[field['uuid']]).each do |uuid| %>
<% attachment = ActiveStorage::Attachment.find_by(uuid:) %>
<li class="flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6">
<div class="flex w-0 flex-1 items-center">
<%= svg_icon('paperclip', class: 'h-5 w-5 flex-shrink-0 text-gray-300') %>
<div class="ml-4 flex min-w-0 flex-1 gap-2">
<span class="truncate font-medium">
<%= attachment.filename %>
</span>
<% @submission.submitters.each do |submitter| %>
<% Array.wrap(submitter.values[field['uuid']]).each do |uuid| %>
<% attachment = ActiveStorage::Attachment.find_by(uuid:) %>
<li class="flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6">
<div class="flex w-0 flex-1 items-center">
<%= svg_icon('paperclip', class: 'h-5 w-5 flex-shrink-0 text-gray-300') %>
<div class="ml-4 flex min-w-0 flex-1 gap-2">
<span class="truncate font-medium">
<%= attachment.filename %>
</span>
</div>
</div>
<div class="ml-4 flex-shrink-0">
<%= link_to 'Download', attachment.url, class: "font-medium text-indigo-600 hover:text-indigo-500" %>
</div>
</div>
<div class="ml-4 flex-shrink-0">
<%= link_to 'Download', attachment.url, class: "font-medium text-indigo-600 hover:text-indigo-500" %>
</div>
</li>
</li>
<% end %>
<% end %>
</ul>
<% else %>
<%= @submission.values[field['uuid']] %>
<% @submission.submitters.each do |submitter| %>
<%= submitter.values[field['uuid']] %>
<% end %>
<% end %>
</dt>
</div>

@ -1,5 +1,5 @@
<p>
Form completed - thanks!
</p>
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending'), send_submission_email_index_path, params: { submission_slug: @submission.slug }, form: { onsubmit: 'event.submitter.disabled = true' } %>
<%= button_to button_title(title: 'Download documents', disabled_with: 'Downloading'), submission_download_index_path(@submission.slug), method: :get, form: { onsubmit: 'event.submitter.disabled = true' } %>
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending'), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' } %>
<%= button_to button_title(title: 'Download documents', disabled_with: 'Downloading'), submitter_download_index_path(@submitter.slug), method: :get, form: { onsubmit: 'event.submitter.disabled = true' } %>

@ -1,8 +1,8 @@
<% attachment_field_uuids = @submission.template.fields.select { |f| f['type'].in?(%w[image signature attachment]) }.pluck('uuid') %>
<% attachments = ActiveStorage::Attachment.where(uuid: @submission.values.values_at(*attachment_field_uuids).flatten).preload(:blob) %>
<% attachment_field_uuids = @submitter.submission.template.fields.select { |f| f['type'].in?(%w[image signature attachment]) }.pluck('uuid') %>
<% attachments = ActiveStorage::Attachment.where(uuid: @submitter.values.values_at(*attachment_field_uuids).flatten).preload(:blob) %>
<div class="mx-auto block" style="max-width: 1000px">
<% @submission.template.schema.each do |item| %>
<% document = @submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
<% @submitter.submission.template.schema.each do |item| %>
<% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div class="relative">
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy">
@ -12,7 +12,7 @@
<% end %>
<div class="sticky bottom-8 w-full">
<div class="bg-white mx-8 md:mx-32 border p-4 rounded">
<submission-form data-submission-slug="<%= @submission.slug %>" data-attachments="<%= attachments.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submission.template.fields.to_json %>" data-values="<%= @submission.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
<submission-form data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-attachments="<%= attachments.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submitter.submission.template.fields.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
</div>
</div>
</div>

@ -0,0 +1,3 @@
<p>Hi</a>
<%= @submitter.values %>
<%= link_to 'Download', submitter_download_index_url(@submitter.slug) %>

@ -1,4 +1,4 @@
<p>Hi there</p>
<p>Hi there,</p>
<p>You have been invited to submit a form:</p>
<p><%= link_to 'Submit', submit_form_index_url(slug: @submission.slug) %></p>
<p><%= link_to 'Submit', submit_form_url(slug: @submitter.slug) %></p>
<p>If you didn't request this, please ignore this email.</p>

@ -13,7 +13,7 @@ Rails.configuration.to_prepare do
LoadActiveStorageConfigs.call
rescue StandardError => e
Rails.logger.debug(e)
Rails.logger.error(e)
nil
end

@ -10,10 +10,16 @@ ActionView::Base.field_error_proc = proc do |html_tag, instance|
parsed_html_tag = Nokogiri::HTML::DocumentFragment.parse(html_tag)
parsed_html_tag.children.add_class 'input-error'
# rubocop:disable Rails/OutputSafety
html_tag = parsed_html_tag.to_s.html_safe
# rubocop:enable Rails/OutputSafety
result = html_tag
result += ApplicationController.helpers.tag.label(ApplicationController.render(partial: 'shared/field_error', locals: { message: "#{field_name} #{errors}" }), class: 'label') if errors.present?
if errors.present?
result +=
ApplicationController.render(partial: 'shared/field_error', locals: { message: "#{field_name} #{errors}" })
end
result
end

@ -34,11 +34,11 @@ Rails.application.routes.draw do
resources :submissions, only: %i[index new create]
end
resources :start_form, only: %i[show update], path: 'f', param: 'slug' do
resources :start_form, only: %i[show update], path: 't', param: 'slug' do
get :completed
end
resources :submit_form, only: %i[show update], path: 'l', param: 'slug' do
resources :submit_form, only: %i[show update], path: 's', param: 'slug' do
get :completed
end
@ -46,7 +46,7 @@ Rails.application.routes.draw do
get :success, on: :collection
end
resources :submissions, only: %i[], param: 'slug' do
resources :submitters, only: %i[], param: 'slug' do
resources :download, only: %i[index], controller: 'submissions_download'
resources :debug, only: %i[index], controller: 'submissions_debug'
end

@ -3,16 +3,8 @@
class CreateSubmissions < ActiveRecord::Migration[7.0]
def change
create_table :submissions do |t|
t.string :email, null: false, index: true
t.string :slug, null: false, index: { unique: true }
t.references :template, null: false, foreign_key: true, index: true
t.string :values, null: false
t.string :ua
t.string :ip
t.datetime :sent_at
t.datetime :opened_at
t.datetime :completed_at
t.datetime :deleted_at
t.timestamps

@ -0,0 +1,22 @@
# frozen_string_literal: true
class CreateSubmitters < ActiveRecord::Migration[7.0]
def change
create_table :submitters do |t|
t.references :submission, null: false, foreign_key: true, index: true
t.string :uuid, null: false
t.string :email, null: false, index: true
t.string :slug, null: false, index: { unique: true }
t.string :values, null: false
t.string :ua
t.string :ip
t.datetime :sent_at
t.datetime :opened_at
t.datetime :completed_at
t.timestamps
end
end
end

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_05_19_144036) do
ActiveRecord::Schema[7.0].define(version: 2023_06_12_182744) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -61,21 +61,29 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_19_144036) do
end
create_table "submissions", force: :cascade do |t|
t.bigint "template_id", null: false
t.datetime "deleted_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["template_id"], name: "index_submissions_on_template_id"
end
create_table "submitters", force: :cascade do |t|
t.bigint "submission_id", null: false
t.string "uuid", null: false
t.string "email", null: false
t.string "slug", null: false
t.bigint "template_id", null: false
t.string "values", null: false
t.string "ua"
t.string "ip"
t.datetime "sent_at"
t.datetime "opened_at"
t.datetime "completed_at"
t.datetime "deleted_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_submissions_on_email"
t.index ["slug"], name: "index_submissions_on_slug", unique: true
t.index ["template_id"], name: "index_submissions_on_template_id"
t.index ["email"], name: "index_submitters_on_email"
t.index ["slug"], name: "index_submitters_on_slug", unique: true
t.index ["submission_id"], name: "index_submitters_on_submission_id"
end
create_table "templates", force: :cascade do |t|
@ -125,6 +133,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_19_144036) do
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "encrypted_configs", "accounts"
add_foreign_key "submissions", "templates"
add_foreign_key "submitters", "submissions"
add_foreign_key "templates", "accounts"
add_foreign_key "templates", "users", column: "author_id"
add_foreign_key "users", "accounts"

@ -8,19 +8,18 @@ module Submissions
module_function
# rubocop:disable Metrics
def call(submission)
cert = submission.template.account.encrypted_configs
.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
def call(submitter)
template = submitter.submission.template
cert = submitter.submission.template.account.encrypted_configs
.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
zip_file = Tempfile.new
zip_stream = Zip::ZipOutputStream.open(zip_file)
pdfs_index =
submission.template.documents.to_h do |attachment|
[attachment.uuid, HexaPDF::Document.new(io: StringIO.new(attachment.download))]
end
pdfs_index = build_pdfs_index(submitter)
submission.template.fields.each do |field|
template.fields.each do |field|
field.fetch('areas', []).each do |area|
pdf = pdfs_index[area['attachment_uuid']]
@ -29,7 +28,7 @@ module Submissions
width = page.box.width
height = page.box.height
value = submission.values[field['uuid']]
value = submitter.values[field['uuid']]
canvas = page.canvas(type: :overlay)
@ -88,7 +87,8 @@ module Submissions
end
when 'date'
canvas.font(FONT_NAME, size: FONT_SIZE)
canvas.text(I18n.l(Date.parse(value)), at: [area['x'] * width, height - ((area['y'] * height) + FONT_SIZE)])
canvas.text(I18n.l(Date.parse(value)),
at: [area['x'] * width, height - ((area['y'] * height) + FONT_SIZE)])
else
canvas.font(FONT_NAME, size: FONT_SIZE)
canvas.text(value.to_s, at: [area['x'] * width, height - ((area['y'] * height) + FONT_SIZE)])
@ -96,14 +96,14 @@ module Submissions
end
end
submission.template.schema.map do |item|
document = submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] }
template.schema.map do |item|
template.documents.find { |a| a.uuid == item['attachment_uuid'] }
io = StringIO.new
pdf = pdfs_index[item['attachment_uuid']]
pdf.sign(io, reason: "Signed by #{submission.email}",
pdf.sign(io, reason: "Signed by #{submitter.email}",
# doc_mdp_permissions: :no_changes,
certificate: OpenSSL::X509::Certificate.new(cert['cert']),
key: OpenSSL::PKey::RSA.new(cert['key']),
@ -113,13 +113,33 @@ module Submissions
zip_stream.put_next_entry("#{item['name']}.pdf")
zip_stream.write(io.string)
submission.documents.attach(io: StringIO.new(io.string), filename: document.filename)
ActiveStorage::Attachment.create!(
uuid: item['attachment_uuid'],
blob: ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(io.string), filename: "#{item['name']}.pdf"
),
name: 'documents',
record: submitter
)
end
zip_stream.close
submission.archive.attach(io: zip_file, filename: "#{submission.template.name}.zip")
submitter.archive.attach(io: zip_file, filename: "#{template.name}.zip")
end
# rubocop:enable Metrics
def build_pdfs_index(submitter)
latest_submitter = submitter.submission.submitters
.select { |e| e.id != submitter.id && e.completed_at? }
.max_by(&:completed_at)
documents = latest_submitter&.documents.to_a.presence
documents ||= submitter.submission.template.documents
documents.to_h do |attachment|
[attachment.uuid, HexaPDF::Document.new(io: StringIO.new(attachment.download))]
end
end
end
end

Loading…
Cancel
Save