From a29e09ed2f5fd9e6f34a16c3b9b0a550663e2d12 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 8 Jul 2026 10:20:42 +0300 Subject: [PATCH] add submission update api --- app/controllers/api/submissions_controller.rb | 40 ++++++++++++- app/controllers/api/templates_controller.rb | 2 +- app/controllers/start_form_controller.rb | 7 ++- app/jobs/process_submission_expired_job.rb | 1 + config/routes.rb | 2 +- lib/submissions.rb | 3 +- lib/submissions/create_from_submitters.rb | 3 +- .../process_submission_expired_job_spec.rb | 51 +++++++++++++++++ spec/requests/submissions_spec.rb | 56 +++++++++++++++++++ 9 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 spec/jobs/process_submission_expired_job_spec.rb diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index ca996d32..9c7e6911 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -7,7 +7,7 @@ module Api TEMPLATE_COLUMNS = %i[id name external_id created_at updated_at folder_id submitters].freeze load_and_authorize_resource :template, only: :create - load_and_authorize_resource :submission, only: %i[show index destroy] + load_and_authorize_resource :submission, only: %i[show index update destroy] before_action only: :create do authorize!(:create, Submission) @@ -104,6 +104,25 @@ module Api render json: { error: e.message }, status: :unprocessable_content end + def update + @submission = assign_submission_attrs(@submission, submission_params) + + @submission.save! + + if @submission.saved_change_to_archived_at? && @submission.archived_at? + WebhookUrls.enqueue_events(@submission, 'submission.archived') + end + + if @submission.saved_change_to_expire_at? && @submission.expire_at? + ProcessSubmissionExpiredJob.perform_at(@submission.expire_at, 'submission_id' => @submission.id, + 'expire_at' => @submission.expire_at.to_i) + end + + SearchEntries.enqueue_reindex(@submission) if @submission.saved_change_to_name? + + render json: Submissions::SerializeForApi.call(@submission, nil, params, with_events: false) + end + def destroy if params[:permanently].in?(['true', true]) @submission.destroy! @@ -118,6 +137,25 @@ module Api private + def assign_submission_attrs(submission, attrs) + archived = attrs.key?(:archived) ? attrs[:archived] : attrs[:archived_at] + + if archived.in?([true, false, 'true', 'false']) && current_ability.can?(:destroy, submission) + submission.archived_at = archived.in?(Submitters::TRUE_VALUES) ? Time.current : nil + end + + submission.name = attrs[:name] if attrs.key?(:name) + submission.expire_at = attrs[:expire_at].presence if attrs.key?(:expire_at) + + submission + end + + def submission_params + submission_params = params.key?(:submission) ? params.require(:submission) : params + + submission_params.permit(:name, :expire_at, :archived, :archived_at) + end + def maybe_return_template_error return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil? diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index bda6b8d2..0a989e55 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -56,7 +56,7 @@ module Api @template.update!(template_params) - SearchEntries.enqueue_reindex(@template) + SearchEntries.enqueue_reindex(@template) if @template.saved_change_to_name? WebhookUrls.enqueue_events(@template, 'template.updated') diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 4a0d7890..5d3614f2 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -93,9 +93,12 @@ class StartFormController < ApplicationController SearchEntries.enqueue_reindex(submitter) - return unless submitter.submission.expire_at? + expire_at = submitter.submission.expire_at - ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id) + return unless expire_at + + ProcessSubmissionExpiredJob.perform_at(expire_at, 'submission_id' => submitter.submission_id, + 'expire_at' => expire_at.to_i) end def load_resubmit_submitter diff --git a/app/jobs/process_submission_expired_job.rb b/app/jobs/process_submission_expired_job.rb index b3838c00..dde2853a 100644 --- a/app/jobs/process_submission_expired_job.rb +++ b/app/jobs/process_submission_expired_job.rb @@ -12,6 +12,7 @@ class ProcessSubmissionExpiredJob return if submission.template&.archived_at? return if submission.submitters.where.not(declined_at: nil).exists? return if submission.completed_at? + return if params['expire_at'] && submission.expire_at&.to_i != params['expire_at'] WebhookUrls.enqueue_events(submission, 'submission.expired') end diff --git a/config/routes.rb b/config/routes.rb index e1100e04..0127450e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,7 +29,7 @@ Rails.application.routes.draw do resources :submitter_email_clicks, only: %i[create] resources :submitter_form_views, only: %i[create] resources :submitters, only: %i[index show update] - resources :submissions, only: %i[index show create destroy] do + resources :submissions, only: %i[index show create update destroy] do resources :documents, only: %i[index], controller: 'submission_documents' collection do resources :init, only: %i[create], controller: 'submissions' diff --git a/lib/submissions.rb b/lib/submissions.rb index 0e36e45d..08a44e42 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -132,7 +132,8 @@ module Submissions submission.save! if submission.expire_at? - ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id) + ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id, + 'expire_at' => submission.expire_at.to_i) end submission diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index aec48a3f..7332ce9b 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -178,7 +178,8 @@ module Submissions submissions.each do |submission| next unless submission.expire_at? - ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id) + ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id, + 'expire_at' => submission.expire_at.to_i) end end diff --git a/spec/jobs/process_submission_expired_job_spec.rb b/spec/jobs/process_submission_expired_job_spec.rb new file mode 100644 index 00000000..bd638de0 --- /dev/null +++ b/spec/jobs/process_submission_expired_job_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +RSpec.describe ProcessSubmissionExpiredJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:expire_at) { 2.days.from_now.change(usec: 0) } + let(:submission) do + create(:submission, :with_submitters, template:, created_by_user: user, expire_at:) + end + + before { allow(WebhookUrls).to receive(:enqueue_events) } + + describe '#perform' do + it 'enqueues the expired event when the scheduled expire_at still matches' do + described_class.new.perform('submission_id' => submission.id, 'expire_at' => expire_at.to_i) + + expect(WebhookUrls).to have_received(:enqueue_events).with(submission, 'submission.expired') + end + + it 'enqueues the expired event for legacy jobs scheduled without an expire_at param' do + described_class.new.perform('submission_id' => submission.id) + + expect(WebhookUrls).to have_received(:enqueue_events).with(submission, 'submission.expired') + end + + it 'skips a stale job scheduled for an earlier expire_at that was extended' do + submission.update!(expire_at: 3.days.from_now) + + described_class.new.perform('submission_id' => submission.id, 'expire_at' => expire_at.to_i) + + expect(WebhookUrls).not_to have_received(:enqueue_events) + end + + it 'skips a stale job scheduled for a later expire_at that was shortened' do + submission.update!(expire_at: 1.day.from_now) + + described_class.new.perform('submission_id' => submission.id, 'expire_at' => expire_at.to_i) + + expect(WebhookUrls).not_to have_received(:enqueue_events) + end + + it 'skips a stale job when the expiration has been cleared' do + submission.update!(expire_at: nil) + + described_class.new.perform('submission_id' => submission.id, 'expire_at' => expire_at.to_i) + + expect(WebhookUrls).not_to have_received(:enqueue_events) + end + end +end diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb index 351aced9..3adf6092 100644 --- a/spec/requests/submissions_spec.rb +++ b/spec/requests/submissions_spec.rb @@ -259,6 +259,62 @@ describe 'Submission API' do end end + describe 'PUT /api/submissions/:id' do + it 'updates the submission name' do + submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author) + + put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: { + name: 'Updated Name' + }.to_json + + expect(response).to have_http_status(:ok) + expect(submission.reload.name).to eq('Updated Name') + expect(response.parsed_body['name']).to eq('Updated Name') + end + + it 'updates the expiration date' do + submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author) + expire_at = 1.week.from_now.change(usec: 0) + + put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: { + expire_at: expire_at.iso8601 + }.to_json + + expect(response).to have_http_status(:ok) + expect(submission.reload.expire_at).to be_within(1.second).of(expire_at) + end + + it 'clears the expiration date when passed nil' do + submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author, + expire_at: 1.week.from_now) + + put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: { + expire_at: nil + }.to_json + + expect(response).to have_http_status(:ok) + expect(submission.reload.expire_at).to be_nil + end + + it 'archives and unarchives the submission' do + submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author) + + put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: { + archived: true + }.to_json + + expect(response).to have_http_status(:ok) + expect(submission.reload.archived_at).not_to be_nil + + put "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }, params: { + archived: false + }.to_json + + expect(response).to have_http_status(:ok) + expect(submission.reload.archived_at).to be_nil + end + end + describe 'view-only (CC) party' do let(:viewer_template) { create(:template, account:, author:, submitter_count: 2, only_field_types: %w[text]) } let(:viewer_uuid) { viewer_template.submitters.second['uuid'] }