diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index a83a1e5b..aa4db085 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -147,6 +147,7 @@ module Api def submissions_params permitted_attrs = [ :send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last, + :expire_at, { message: %i[subject body], submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index d5f033c2..3d73410d 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -17,7 +17,8 @@ class StartFormController < ApplicationController def update return redirect_to start_form_path(@template.slug) if @template.archived_at? - @submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil)) + @submitter = Submitter.where(submission: @template.submissions.where(expire_at: Time.current..) + .or(@template.submissions.where(expire_at: nil)).where(archived_at: nil)) .order(id: :desc) .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } .find_or_initialize_by(**submitter_params.compact_blank) diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index 7f32f5d5..87cc217f 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -14,6 +14,7 @@ class SubmitFormController < ApplicationController return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at? + return render :expired if @submitter.submission.expired? Submitters.preload_with_pages(@submitter) @@ -51,6 +52,10 @@ class SubmitFormController < ApplicationController return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity end + if submitter.submission.expired? + return render json: { error: 'Form has been expired.' }, status: :unprocessable_entity + end + Submitters::SubmitValues.call(submitter, params, request) head :ok diff --git a/app/models/submission.rb b/app/models/submission.rb index 55bb8fcd..353af713 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -6,6 +6,7 @@ # # id :bigint not null, primary key # archived_at :datetime +# expire_at :datetime # preferences :text not null # slug :string not null # source :text not null @@ -75,6 +76,10 @@ class Submission < ApplicationRecord preserved: 'preserved' }, scope: false, prefix: true + def expired? + expire_at && expire_at <= Time.current + end + def audit_trail_url return if audit_trail.blank? diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 4fe01ca2..41d3b29e 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -54,7 +54,7 @@ <% end %> - <% elsif @submission.submitters.to_a.size == 1 %> + <% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? %> <%= render 'shared/clipboard_copy', text: start_form_url(slug: @submission.template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard' %> <% end %> @@ -146,15 +146,15 @@ <%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) : 'Not completed yet' %> - <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && Accounts.can_send_emails?(current_account) %> + <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && Accounts.can_send_emails?(current_account) && !@submission.expired? %>
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
<% end %> - <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) %> + <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && !@submission.expired? %> <%= render 'submissions/send_sms_button', submitter: %> <% end %> - <% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, submitter) %> + <% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, submitter) && !@submission.expired? %>
Sign In-person diff --git a/app/views/submit_form/expired.html.erb b/app/views/submit_form/expired.html.erb new file mode 100644 index 00000000..8a85d5e8 --- /dev/null +++ b/app/views/submit_form/expired.html.erb @@ -0,0 +1,21 @@ +
+
+
+
+ <%= render 'start_form/banner' %> +
+
+
+
+ <%= svg_icon('writing_sign', class: 'w-10 h-10') %> +
+
+

<%= @submitter.submission.template.name %>

+

<%= t('form_expired_at_html', time: l(@submitter.submission.expire_at, format: :long)) %>

+
+
+
+
+
+
+<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %> diff --git a/app/views/templates/_submission.html.erb b/app/views/templates/_submission.html.erb index e24e5eed..78fdbca3 100644 --- a/app/views/templates/_submission.html.erb +++ b/app/views/templates/_submission.html.erb @@ -34,11 +34,19 @@ <% submitter = submitters.first %>
-
- - <%= submitter.status %> - -
+ <% if submission.expired? %> +
+ + Expired + +
+ <% else %> +
+ + <%= submitter.status %> + +
+ <% end %> <%= submitter.name || submitter.email || submitter.phone %> @@ -61,7 +69,7 @@ - <% elsif !submission.archived_at? && !template.archived_at? %> + <% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? %> <% if current_user.email == submitter.email %>
+ <% elsif submission.expired? %> +
+ + Expired + +
<% end %>
<% submitters.each_with_index do |submitter, index| %>
- <% unless is_submission_completed %> + <% if !is_submission_completed && !submission.expired? %>
<%= submitter.status %> @@ -132,7 +146,7 @@ - <% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed %> + <% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? %>
<% if current_user.email == submitter.email %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index 94f818ad..942bfd1e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,6 +19,7 @@ en: &en language_ar: العربية language_ko: 한국어 email: Email + form_expired_at_html: 'Form expired on %{time}' verification_code_code: 'Verification code: %{code}' digitally_signed_by: Digitally signed by role: Role @@ -66,6 +67,7 @@ en: &en es: &es role: Rol reason: Razón + form_expired_at_html: 'El formulario expiró el %{time}' verification_code_code: 'Código de verificación: %{code}' email: Correo electrónico digitally_signed_by: Firmado digitalmente por @@ -111,6 +113,7 @@ it: &it reason: Ragione verification_code_code: 'Codice di verifica: %{code}' email: Email + form_expired_at_html: 'Il modulo è scaduto il %{time}' digitally_signed_by: Firmato digitalmente da role: Ruolo provide_your_email_to_start: Fornisci la tua email per iniziare @@ -154,6 +157,7 @@ fr: &fr digitally_signed_by: Signé numériquement par reason: Raison role: Rôle + form_expired_at_html: 'Le formulaire a expiré le %{time}' provide_your_email_to_start: Entrez votre adresse email pour commencer start: Démarrer starting: Démarrage @@ -196,6 +200,7 @@ pt: &pt reason: Razão verification_code_code: 'Código de verificação: %{code}' digitally_signed_by: Assinado digitalmente por + form_expired_at_html: 'O formulário expirou em %{time}' role: Função provide_your_email_to_start: Forneça o seu email para começar start: Iniciar @@ -237,6 +242,7 @@ de: &de verification_code_code: 'Verifizierungscode: %{code}' reason: Grund email: E-Mail + form_expired_at_html: 'Das Formular ist am %{time} abgelaufen' digitally_signed_by: Digital signiert von role: Rolle provide_your_email_to_start: Gib deine E-Mail-Adresse ein, um zu starten @@ -278,6 +284,7 @@ pl: email: Email verification_code_code: 'Kod weryfikacyjny: %{code}' digitally_signed_by: Podpis cyfrowy przez + form_expired_at_html: 'Formularz wygasł o %{time}' role: Rola provide_your_email_to_start: Podaj swój adres email, aby rozpocząć start: Rozpocznij @@ -321,6 +328,7 @@ uk: verification_code_code: 'Код підтвердження: %{code}' role: Роль provide_your_email_to_start: Введіть свій email, щоб почати + form_expired_at_html: 'Строк подачі завершився о %{time}' start: Почати reason: Причина starting: Початок @@ -363,6 +371,7 @@ cs: role: Role provide_your_email_to_start: Zadejte svůj email pro zahájení reason: Důvod + form_expired_at_html: 'Formulář vypršel %{time}' start: Zahájit starting: Zahajování form_has_been_deleted_by_html: 'Formulář byl smazán uživatelem %{name}.' @@ -406,6 +415,7 @@ he: provide_your_email_to_start: ספק את כתובת הדוא"ל שלך כדי להתחיל start: התחל starting: מתחיל + form_expired_at_html: 'הטופס פג תוקף ב- %{time}' form_has_been_deleted_by_html: 'הטופס נמחק על ידי %{name}.' invited_by_html: 'הוזמן על ידי %{name}' you_have_been_invited_to_submit_a_form: הוזמנת להגיש טופס @@ -446,6 +456,7 @@ nl: provide_your_email_to_start: Geef uw e-mailadres om te beginnen start: Start reason: Reden + form_expired_at_html: 'Formulier is verlopen op %{time}' starting: Starten form_has_been_deleted_by_html: 'Formulier is verwijderd door %{name}.' invited_by_html: 'Uitgenodigd door %{name}' @@ -491,6 +502,7 @@ ar: form_has_been_deleted_by_html: 'تم حذف الاستمارة بواسطة %{name}.' reason: سبب invited_by_html: 'تمت الدعوة بواسطة %{name}' + form_expired_at_html: 'استمارة انتهت صلاحيتها في %{time}' you_have_been_invited_to_submit_a_form: تمت دعوتك لتقديم استمارة signed_on_time: 'تم التوقيع في %{time}' form_has_been_submitted_already: تم تقديم الاستمارة بالفعل @@ -530,6 +542,7 @@ ko: start: 시작 reason: 이유 starting: 시작 중 + form_expired_at_html: '양식이 만료되었습니다 %{time}' form_has_been_deleted_by_html: '%{name}에 의해 양식이 삭제되었습니다.' invited_by_html: '%{name}에 의해 초대되었습니다.' you_have_been_invited_to_submit_a_form: 양식을 제출하도록 초대되었습니다. diff --git a/db/migrate/20240801125558_add_expire_at_to_submissions.rb b/db/migrate/20240801125558_add_expire_at_to_submissions.rb new file mode 100644 index 00000000..efb71a89 --- /dev/null +++ b/db/migrate/20240801125558_add_expire_at_to_submissions.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddExpireAtToSubmissions < ActiveRecord::Migration[7.1] + def change + add_column :submissions, :expire_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 21e94cef..89a48c9e 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.1].define(version: 2024_07_20_063827) do +ActiveRecord::Schema[7.1].define(version: 2024_08_01_125558) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -218,6 +218,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_20_063827) do t.string "slug", null: false t.text "preferences", null: false t.bigint "account_id", null: false + t.datetime "expire_at" t.index ["account_id"], name: "index_submissions_on_account_id" t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id" t.index ["slug"], name: "index_submissions_on_slug", unique: true diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index 77e878ce..9cbab22d 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -17,6 +17,7 @@ module Submissions submission = template.submissions.new(created_by_user: user, source:, account_id: user.account_id, preferences: set_submission_preferences, + expire_at: attrs[:expire_at], template_submitters: [], submitters_order:) maybe_set_template_fields(submission, attrs[:submitters]) diff --git a/lib/submissions/serialize_for_api.rb b/lib/submissions/serialize_for_api.rb index 19efac2c..bfe36f7b 100644 --- a/lib/submissions/serialize_for_api.rb +++ b/lib/submissions/serialize_for_api.rb @@ -3,7 +3,7 @@ module Submissions module SerializeForApi SERIALIZE_PARAMS = { - only: %i[id slug source submitters_order created_at updated_at archived_at], + only: %i[id slug source submitters_order expire_at created_at updated_at archived_at], methods: %i[audit_log_url], include: { submitters: { only: %i[id slug uuid name email phone @@ -41,7 +41,7 @@ module Submissions json[:completed_at] = last_submitter.completed_at else json[:documents] = [] - json[:status] = 'pending' + json[:status] = submission.expired? ? 'expired' : 'pending' json[:completed_at] = nil end diff --git a/lib/time_utils.rb b/lib/time_utils.rb index f290671f..1f499195 100644 --- a/lib/time_utils.rb +++ b/lib/time_utils.rb @@ -32,6 +32,14 @@ module TimeUtils tz_info.abbreviation(time) end + def parse_time_value(value) + if value.is_a?(Integer) + Time.zone.at(value.to_s.first(10).to_i) + elsif value.present? + Time.zone.parse(value) + end + end + def parse_date_string(string, pattern) pattern = pattern.sub(/Y+/, YEAR_FORMATS) .sub(/M+/, MONTH_FORMATS)