store multiple submitter results

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

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

@ -5,14 +5,14 @@ module Api
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
def create 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]) blob = ActiveStorage::Blob.find_signed(params[:blob_signed_id])
attachment = ActiveStorage::Attachment.create!( attachment = ActiveStorage::Attachment.create!(
blob:, blob:,
name: params[:name], 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]) 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 success; end
def create def create
@submission = @submitter =
if params[:template_slug] 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 else
Submission.find_by!(slug: params[:submission_slug]) Submitter.find_by!(slug: params[:submitter_slug])
end end
SubmissionMailer.copy_to_submitter(@submission).deliver_later! SubmitterMailer.copy_to_submitter(@submitter).deliver_later!
respond_to do |f| respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path } f.html { redirect_to success_send_submission_email_index_path }

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

@ -17,18 +17,18 @@ class SubmissionsController < ApplicationController
def new; end def new; end
def create def create
emails = params[:emails].to_s.scan(User::EMAIL_REGEXP)
submissions = submissions =
emails.map do |email| if params[:emails].present?
submission = @template.submissions.create!(email:, sent_at: params[:send_email] == '1' ? Time.current : nil) create_submissions_from_emails
else
if params[:send_email] == '1' create_submissions_from_submitters
SubmissionMailer.invitation_email(submission, message: params[:message]).deliver_later! end
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
end
redirect_to template_submissions_path(@template), notice: "#{submissions.size} recepients added" redirect_to template_submissions_path(@template), notice: "#{submissions.size} recepients added"
end end
@ -44,6 +44,34 @@ class SubmissionsController < ApplicationController
private 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 def load_template
@template = current_account.templates.find(params[:template_id]) @template = current_account.templates.find(params[:template_id])
end end

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

@ -4,10 +4,10 @@ class SubmissionsDownloadController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
def index 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
end end

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

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

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

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

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

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

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

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

@ -39,7 +39,7 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
submissionSlug: { submitterSlug: {
type: String, type: String,
required: true required: true
}, },
@ -81,7 +81,7 @@ export default {
fetch('/api/attachments', { fetch('/api/attachments', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
submission_slug: this.submissionSlug, submitter_slug: this.submitterSlug,
blob_signed_id: data.signed_id, blob_signed_id: data.signed_id,
name: 'attachments' 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 # Table name: submissions
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# completed_at :datetime # deleted_at :datetime
# deleted_at :datetime # created_at :datetime not null
# email :string not null # updated_at :datetime not null
# ip :string # template_id :bigint not null
# 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
# #
# Indexes # Indexes
# #
# index_submissions_on_email (email)
# index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id) # index_submissions_on_template_id (template_id)
# #
# Foreign Keys # Foreign Keys
@ -31,27 +21,7 @@
class Submission < ApplicationRecord class Submission < ApplicationRecord
belongs_to :template belongs_to :template
attribute :values, :string, default: -> { {} } has_many :submitters, dependent: :destroy
attribute :slug, :string, default: -> { SecureRandom.base58(8) }
serialize :values, JSON
has_one_attached :archive
has_many_attached :documents
has_many_attached :attachments
scope :active, -> { where(deleted_at: nil) } 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 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 %> 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 Provide youe email to start
<div> <div>
<%= f.label :email %> <%= 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 %> <%= @template.name %>
<%= link_to 'Edit', template_path(@template), class: 'btn btn-outline btn-sm' %> <%= link_to 'Edit', template_path(@template), class: 'btn btn-outline btn-sm' %>
</h2> </h2>
<div class="join w-full"> <% if @template.submitters.size == 1 %>
<buttun class="btn bg-neutral btn-disabled text-white join-item"> <div class="join w-full">
Share link <buttun class="btn bg-neutral btn-disabled text-white join-item">
</buttun> Share link
<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> </buttun>
<clipboard-copy class="btn btn-neutral btn-square join-item text-white font-bold swap swap-active" for="share-link-input"> <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>
<%= svg_icon('clipboard', class: 'w-6 h-6 swap-on text-white') %> <clipboard-copy class="btn btn-neutral btn-square join-item text-white font-bold swap swap-active" for="share-link-input">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 swap-off text-white') %> <%= svg_icon('clipboard', class: 'w-6 h-6 swap-on text-white') %>
</clipboard-copy> <%= svg_icon('clipboard_copy', class: 'w-6 h-6 swap-off text-white') %>
</div> </clipboard-copy>
</div>
<% end %>
</div> </div>
</div> </div>
<div class="flex justify-between mb-4"> <div class="flex justify-between mb-4">
@ -42,15 +44,22 @@
<% @submissions.each do |submission| %> <% @submissions.each do |submission| %>
<tr> <tr>
<td> <td>
<%= submission.email %> <% submission.submitters.each do |submitter| %>
<%= submitter.email %>
<% end %>
</td> </td>
<td> <td>
<span class="badge badge-info badge-outline"> <% submission.submitters.each do |submitter| %>
<%= submission.status.humanize %> <div>
</span> <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>
<td class="flex items-center space-x-2 justify-end"> <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?' } %> <%= 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> </td>
</tr> </tr>

@ -1,9 +1,41 @@
<%= render 'shared/turbo_modal', title: 'New Recepients' do %> <%= 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| %> <%= form_for '', url: template_submissions_path(@template), html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
<div class="form-control"> <% if @template.submitters.size == 1 %>
<%= f.label :emails, class: 'label' %> <div class="form-control">
<%= f.text_area :emails, required: true, class: 'base-textarea' %> <%= f.label :emails, class: 'label' %>
</div> <%= 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"> <div class="form-control">
<%= f.label :send_email, class: 'flex items-center cursor-pointer' do %> <%= 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)" %> <%= f.check_box :send_email, class: 'base-checkbox', onchange: "message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
@ -12,23 +44,18 @@
</div> </div>
<div id="message_field" class="card card-compact bg-base-200 hidden"> <div id="message_field" class="card card-compact bg-base-200 hidden">
<div class="card-body"> <div class="card-body">
<div class="form-control"> <div class="form-control space-y-2">
<label class="label"> <span class="label-text">Hi there,</span>
<span class="label-text">Hi There,</span> <%= f.text_area :message, value: SubmitterMailer::DEFAULT_MESSAGE, required: true, class: 'base-textarea !rounded-lg' %>
</label> <span class="label-text">
<%= f.text_area :message, value: SubmissionMailer::DEFAULT_MESSAGE, required: true, class: 'base-textarea !rounded-lg' %> Thanks,
<label class="label"> </br>
<span class="label-text"> <%= current_account.name %>
Thanks, </div>
</br>
<%= current_account.name %>
</span>
</label>
</div> </div>
</div> </div>
</div> <div class="form-control">
<div class="form-control"> <%= f.button button_title(title: 'Confirm', disabled_with: 'Processing'), class: 'base-button' %>
<%= f.button button_title(title: 'Confirm', disabled_with: 'Processing'), class: 'base-button' %> </div>
</div> <% end %>
<% end %>
<% end %> <% end %>

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

@ -1,5 +1,5 @@
<p> <p>
Form completed - thanks! Form completed - thanks!
</p> </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: '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'), submission_download_index_path(@submission.slug), method: :get, 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') %> <% attachment_field_uuids = @submitter.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) %> <% attachments = ActiveStorage::Attachment.where(uuid: @submitter.values.values_at(*attachment_field_uuids).flatten).preload(:blob) %>
<div class="mx-auto block" style="max-width: 1000px"> <div class="mx-auto block" style="max-width: 1000px">
<% @submission.template.schema.each do |item| %> <% @submitter.submission.template.schema.each do |item| %>
<% document = @submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %> <% 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| %> <% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div class="relative"> <div class="relative">
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy"> <img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy">
@ -12,7 +12,7 @@
<% end %> <% end %>
<div class="sticky bottom-8 w-full"> <div class="sticky bottom-8 w-full">
<div class="bg-white mx-8 md:mx-32 border p-4 rounded"> <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> </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>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> <p>If you didn't request this, please ignore this email.</p>

@ -13,7 +13,7 @@ Rails.configuration.to_prepare do
LoadActiveStorageConfigs.call LoadActiveStorageConfigs.call
rescue StandardError => e rescue StandardError => e
Rails.logger.debug(e) Rails.logger.error(e)
nil nil
end 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 = Nokogiri::HTML::DocumentFragment.parse(html_tag)
parsed_html_tag.children.add_class 'input-error' parsed_html_tag.children.add_class 'input-error'
# rubocop:disable Rails/OutputSafety
html_tag = parsed_html_tag.to_s.html_safe html_tag = parsed_html_tag.to_s.html_safe
# rubocop:enable Rails/OutputSafety
result = html_tag 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 result
end end

@ -34,11 +34,11 @@ Rails.application.routes.draw do
resources :submissions, only: %i[index new create] resources :submissions, only: %i[index new create]
end 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 get :completed
end 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 get :completed
end end
@ -46,7 +46,7 @@ Rails.application.routes.draw do
get :success, on: :collection get :success, on: :collection
end end
resources :submissions, only: %i[], param: 'slug' do resources :submitters, only: %i[], param: 'slug' do
resources :download, only: %i[index], controller: 'submissions_download' resources :download, only: %i[index], controller: 'submissions_download'
resources :debug, only: %i[index], controller: 'submissions_debug' resources :debug, only: %i[index], controller: 'submissions_debug'
end end

@ -3,16 +3,8 @@
class CreateSubmissions < ActiveRecord::Migration[7.0] class CreateSubmissions < ActiveRecord::Migration[7.0]
def change def change
create_table :submissions do |t| 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.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.datetime :deleted_at
t.timestamps 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -61,21 +61,29 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_19_144036) do
end end
create_table "submissions", force: :cascade do |t| 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 "email", null: false
t.string "slug", null: false t.string "slug", null: false
t.bigint "template_id", null: false
t.string "values", null: false t.string "values", null: false
t.string "ua" t.string "ua"
t.string "ip" t.string "ip"
t.datetime "sent_at" t.datetime "sent_at"
t.datetime "opened_at" t.datetime "opened_at"
t.datetime "completed_at" t.datetime "completed_at"
t.datetime "deleted_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["email"], name: "index_submissions_on_email" t.index ["email"], name: "index_submitters_on_email"
t.index ["slug"], name: "index_submissions_on_slug", unique: true t.index ["slug"], name: "index_submitters_on_slug", unique: true
t.index ["template_id"], name: "index_submissions_on_template_id" t.index ["submission_id"], name: "index_submitters_on_submission_id"
end end
create_table "templates", force: :cascade do |t| 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 "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "encrypted_configs", "accounts" add_foreign_key "encrypted_configs", "accounts"
add_foreign_key "submissions", "templates" add_foreign_key "submissions", "templates"
add_foreign_key "submitters", "submissions"
add_foreign_key "templates", "accounts" add_foreign_key "templates", "accounts"
add_foreign_key "templates", "users", column: "author_id" add_foreign_key "templates", "users", column: "author_id"
add_foreign_key "users", "accounts" add_foreign_key "users", "accounts"

@ -8,19 +8,18 @@ module Submissions
module_function module_function
# rubocop:disable Metrics # rubocop:disable Metrics
def call(submission) def call(submitter)
cert = submission.template.account.encrypted_configs template = submitter.submission.template
.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
cert = submitter.submission.template.account.encrypted_configs
.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
zip_file = Tempfile.new zip_file = Tempfile.new
zip_stream = Zip::ZipOutputStream.open(zip_file) zip_stream = Zip::ZipOutputStream.open(zip_file)
pdfs_index = pdfs_index = build_pdfs_index(submitter)
submission.template.documents.to_h do |attachment|
[attachment.uuid, HexaPDF::Document.new(io: StringIO.new(attachment.download))]
end
submission.template.fields.each do |field| template.fields.each do |field|
field.fetch('areas', []).each do |area| field.fetch('areas', []).each do |area|
pdf = pdfs_index[area['attachment_uuid']] pdf = pdfs_index[area['attachment_uuid']]
@ -29,7 +28,7 @@ module Submissions
width = page.box.width width = page.box.width
height = page.box.height height = page.box.height
value = submission.values[field['uuid']] value = submitter.values[field['uuid']]
canvas = page.canvas(type: :overlay) canvas = page.canvas(type: :overlay)
@ -88,7 +87,8 @@ module Submissions
end end
when 'date' when 'date'
canvas.font(FONT_NAME, size: FONT_SIZE) 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 else
canvas.font(FONT_NAME, size: FONT_SIZE) canvas.font(FONT_NAME, size: FONT_SIZE)
canvas.text(value.to_s, at: [area['x'] * width, height - ((area['y'] * height) + 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
end end
submission.template.schema.map do |item| template.schema.map do |item|
document = submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } template.documents.find { |a| a.uuid == item['attachment_uuid'] }
io = StringIO.new io = StringIO.new
pdf = pdfs_index[item['attachment_uuid']] 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, # doc_mdp_permissions: :no_changes,
certificate: OpenSSL::X509::Certificate.new(cert['cert']), certificate: OpenSSL::X509::Certificate.new(cert['cert']),
key: OpenSSL::PKey::RSA.new(cert['key']), key: OpenSSL::PKey::RSA.new(cert['key']),
@ -113,13 +113,33 @@ module Submissions
zip_stream.put_next_entry("#{item['name']}.pdf") zip_stream.put_next_entry("#{item['name']}.pdf")
zip_stream.write(io.string) 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 end
zip_stream.close 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 end
# rubocop:enable Metrics # 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
end end

Loading…
Cancel
Save