diff --git a/.rubocop.yml b/.rubocop.yml index 721e6c95..4406e2f2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -39,6 +39,12 @@ Metrics/PerceivedComplexity: Metrics/AbcSize: Max: 35 +Metrics/ModuleLength: + Max: 500 + +Metrics/ClassLength: + Max: 500 + RSpec/NestedGroups: Max: 6 diff --git a/app/controllers/api/attachments_controller.rb b/app/controllers/api/attachments_controller.rb index 12c6a606..7d13d704 100644 --- a/app/controllers/api/attachments_controller.rb +++ b/app/controllers/api/attachments_controller.rb @@ -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]) diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb index 66bc97fa..f0a40954 100644 --- a/app/controllers/send_submission_email_controller.rb +++ b/app/controllers/send_submission_email_controller.rb @@ -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 } diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index bf4021dc..e7f2d160 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -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 diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 2851e526..3a2d61e6 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -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 diff --git a/app/controllers/submissions_debug_controller.rb b/app/controllers/submissions_debug_controller.rb index 484b0ade..1970e448 100644 --- a/app/controllers/submissions_debug_controller.rb +++ b/app/controllers/submissions_debug_controller.rb @@ -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', diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 23c80505..befde1cc 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -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 diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index 74fbfa73..00ac2b65 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -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 diff --git a/app/javascript/elements/file_dropzone.js b/app/javascript/elements/file_dropzone.js index 8da82e77..24285b19 100644 --- a/app/javascript/elements/file_dropzone.js +++ b/app/javascript/elements/file_dropzone.js @@ -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) => { diff --git a/app/javascript/form.js b/app/javascript/form.js index a766a2a3..020e1757 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -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)), diff --git a/app/javascript/submission_form/attachment_step.vue b/app/javascript/submission_form/attachment_step.vue index c66aafa9..de50cc71 100644 --- a/app/javascript/submission_form/attachment_step.vue +++ b/app/javascript/submission_form/attachment_step.vue @@ -33,7 +33,7 @@ @@ -52,7 +52,7 @@ export default { type: Object, required: true }, - submissionSlug: { + submitterSlug: { type: String, required: true }, diff --git a/app/javascript/submission_form/completed.vue b/app/javascript/submission_form/completed.vue index ce5f007d..6119e38d 100644 --- a/app/javascript/submission_form/completed.vue +++ b/app/javascript/submission_form/completed.vue @@ -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') diff --git a/app/javascript/submission_form/dropzone.vue b/app/javascript/submission_form/dropzone.vue index eaab2aff..ea7bd589 100644 --- a/app/javascript/submission_form/dropzone.vue +++ b/app/javascript/submission_form/dropzone.vue @@ -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) => { diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index a3585094..97d91076 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -1,14 +1,14 @@ @@ -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 } diff --git a/app/javascript/submission_form/image_step.vue b/app/javascript/submission_form/image_step.vue index 003be1da..949a82aa 100644 --- a/app/javascript/submission_form/image_step.vue +++ b/app/javascript/submission_form/image_step.vue @@ -18,7 +18,7 @@ > @@ -37,7 +37,7 @@ export default { type: Object, required: true }, - submissionSlug: { + submitterSlug: { type: String, required: true }, diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index a69e91e2..66714f8e 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -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' }), diff --git a/app/mailers/submission_mailer.rb b/app/mailers/submission_mailer.rb deleted file mode 100644 index 339fc1c4..00000000 --- a/app/mailers/submission_mailer.rb +++ /dev/null @@ -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 diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb new file mode 100644 index 00000000..ea2499e9 --- /dev/null +++ b/app/mailers/submitter_mailer.rb @@ -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 diff --git a/app/models/submission.rb b/app/models/submission.rb index 17d30ce1..e6bde570 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -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 diff --git a/app/models/submitter.rb b/app/models/submitter.rb new file mode 100644 index 00000000..90101f46 --- /dev/null +++ b/app/models/submitter.rb @@ -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 diff --git a/app/views/start_form/show.html.erb b/app/views/start_form/show.html.erb index c55f6725..ddcd6193 100644 --- a/app/views/start_form/show.html.erb +++ b/app/views/start_form/show.html.erb @@ -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
<%= f.label :email %> diff --git a/app/views/submission_mailer/copy_to_submitter.html.erb b/app/views/submission_mailer/copy_to_submitter.html.erb deleted file mode 100644 index 99604333..00000000 --- a/app/views/submission_mailer/copy_to_submitter.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -

Hi -<%= @submission.values %> -<%= link_to 'Download', submission_download_index_url(@submission.slug) %> diff --git a/app/views/submissions/index.html.erb b/app/views/submissions/index.html.erb index 0ea38f5b..116b620e 100644 --- a/app/views/submissions/index.html.erb +++ b/app/views/submissions/index.html.erb @@ -4,16 +4,18 @@ <%= @template.name %> <%= link_to 'Edit', template_path(@template), class: 'btn btn-outline btn-sm' %> -

- - Share link - - - - <%= 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') %> - -
+ <% if @template.submitters.size == 1 %> +
+ + Share link + + + + <%= 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') %> + +
+ <% end %>
@@ -42,15 +44,22 @@ <% @submissions.each do |submission| %> - <%= submission.email %> + <% submission.submitters.each do |submitter| %> + <%= submitter.email %> + <% end %> - - <%= submission.status.humanize %> - + <% submission.submitters.each do |submitter| %> +
+ + <%= submitter.status %> + + <%= link_to 'Copy', submit_form_url(slug: submitter.slug), title: 'Copy', class: 'btn btn-outline btn-xs' %> +
+ <% 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?' } %> diff --git a/app/views/submissions/new.html.erb b/app/views/submissions/new.html.erb index ff2b97e6..e5cffbe5 100644 --- a/app/views/submissions/new.html.erb +++ b/app/views/submissions/new.html.erb @@ -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| %> -
- <%= f.label :emails, class: 'label' %> - <%= f.text_area :emails, required: true, class: 'base-textarea' %> -
+ <% if @template.submitters.size == 1 %> +
+ <%= f.label :emails, class: 'label' %> + <%= f.text_area :emails, required: true, class: 'base-textarea' %> +
+ <% else %> + + + + <% @template.submitters.each do |item| %> + + <% end %> + + + + + + <% @template.submitters.each do |item| %> + + <% end %> + + + +
+ <%= item['name'] %> + +
+ + + + × +
+ + <% end %>
<%= 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 @@
-
- <%= f.button button_title(title: 'Confirm', disabled_with: 'Processing'), class: 'base-button' %> -
-<% end %> +
+ <%= f.button button_title(title: 'Confirm', disabled_with: 'Processing'), class: 'base-button' %> +
+ <% end %> <% end %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 7fbd943d..089b645b 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -12,18 +12,6 @@ <%= link_to @submission.template.name, template_submissions_path(@submission.template), class: 'link link-hover' %>
-
-
URL
-
- <%= link_to submit_form_url(slug: @submission.slug), submit_form_url(slug: @submission.slug), class: 'link link-hover' %> -
-
-
-
Email address
-
- <%= mail_to @submission.email %> -
-
<%- if @submission.completed_at.present? %>
Completed at
@@ -40,35 +28,41 @@
<% if ['image', 'signature'].include?(field['type']) %>
    - <% Array.wrap(@submission.values[field['uuid']]).each do |uuid| %> -
  • -
    - <%= image_tag ActiveStorage::Attachment.find_by(uuid:).url %> -
    -
  • + <% @submission.submitters.each do |submitter| %> + <% Array.wrap(submitter.values[field['uuid']]).each do |uuid| %> +
  • +
    + <%= image_tag ActiveStorage::Attachment.find_by(uuid:).url %> +
    +
  • + <% end %> <% end %>
<% elsif ['attachment'].include?(field['type']) %>
    - <% Array.wrap(@submission.values[field['uuid']]).each do |uuid| %> - <% attachment = ActiveStorage::Attachment.find_by(uuid:) %> -
  • -
    - <%= svg_icon('paperclip', class: 'h-5 w-5 flex-shrink-0 text-gray-300') %> -
    - - <%= attachment.filename %> - + <% @submission.submitters.each do |submitter| %> + <% Array.wrap(submitter.values[field['uuid']]).each do |uuid| %> + <% attachment = ActiveStorage::Attachment.find_by(uuid:) %> +
  • +
    + <%= svg_icon('paperclip', class: 'h-5 w-5 flex-shrink-0 text-gray-300') %> +
    + + <%= attachment.filename %> + +
    +
    +
    + <%= link_to 'Download', attachment.url, class: "font-medium text-indigo-600 hover:text-indigo-500" %>
    -
-
- <%= link_to 'Download', attachment.url, class: "font-medium text-indigo-600 hover:text-indigo-500" %> -
- + + <% end %> <% end %> <% else %> - <%= @submission.values[field['uuid']] %> + <% @submission.submitters.each do |submitter| %> + <%= submitter.values[field['uuid']] %> + <% end %> <% end %> diff --git a/app/views/submit_form/completed.html.erb b/app/views/submit_form/completed.html.erb index f95300a7..88bb2b15 100644 --- a/app/views/submit_form/completed.html.erb +++ b/app/views/submit_form/completed.html.erb @@ -1,5 +1,5 @@

Form completed - thanks!

-<%= 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' } %> diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index 330b96f2..918cf47e 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -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) %>
- <% @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| %>
@@ -12,7 +12,7 @@ <% end %>
- +
diff --git a/app/views/submitter_mailer/copy_to_submitter.html.erb b/app/views/submitter_mailer/copy_to_submitter.html.erb new file mode 100644 index 00000000..611c2398 --- /dev/null +++ b/app/views/submitter_mailer/copy_to_submitter.html.erb @@ -0,0 +1,3 @@ +

Hi +<%= @submitter.values %> +<%= link_to 'Download', submitter_download_index_url(@submitter.slug) %> diff --git a/app/views/submission_mailer/invitation_email.html.erb b/app/views/submitter_mailer/invitation_email.html.erb similarity index 53% rename from app/views/submission_mailer/invitation_email.html.erb rename to app/views/submitter_mailer/invitation_email.html.erb index 3c71c4c1..5dc22b60 100644 --- a/app/views/submission_mailer/invitation_email.html.erb +++ b/app/views/submitter_mailer/invitation_email.html.erb @@ -1,4 +1,4 @@ -

Hi there

+

Hi there,

You have been invited to submit a form:

-

<%= link_to 'Submit', submit_form_index_url(slug: @submission.slug) %>

+

<%= link_to 'Submit', submit_form_url(slug: @submitter.slug) %>

If you didn't request this, please ignore this email.

diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index bf7bf83d..2cf7c277 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -13,7 +13,7 @@ Rails.configuration.to_prepare do LoadActiveStorageConfigs.call rescue StandardError => e - Rails.logger.debug(e) + Rails.logger.error(e) nil end diff --git a/config/initializers/field_error_proc.rb b/config/initializers/field_error_proc.rb index 2393455d..e47d6c49 100644 --- a/config/initializers/field_error_proc.rb +++ b/config/initializers/field_error_proc.rb @@ -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 diff --git a/config/routes.rb b/config/routes.rb index df2ef200..daa8fa4e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20230519144036_create_submissions.rb b/db/migrate/20230519144036_create_submissions.rb index d482bffe..2dcd103d 100644 --- a/db/migrate/20230519144036_create_submissions.rb +++ b/db/migrate/20230519144036_create_submissions.rb @@ -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 diff --git a/db/migrate/20230612182744_create_submitters.rb b/db/migrate/20230612182744_create_submitters.rb new file mode 100644 index 00000000..0bf4a45a --- /dev/null +++ b/db/migrate/20230612182744_create_submitters.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index da90df29..14ac7708 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 014c42ac..643648ff 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -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