From afad3d36f268a8737de39bd2fbd161245b809c4e Mon Sep 17 00:00:00 2001 From: Stephen Daly Date: Wed, 12 Aug 2026 15:35:02 +0100 Subject: [PATCH 1/3] Add method to get latest form document for a form ID and language Returns the live or archived form document for the given form ID and language with the highest version number. --- app/models/form_document.rb | 7 ++++ spec/models/form_document_spec.rb | 67 +++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/app/models/form_document.rb b/app/models/form_document.rb index dd3262259c..25fa387bdd 100644 --- a/app/models/form_document.rb +++ b/app/models/form_document.rb @@ -3,4 +3,11 @@ class FormDocument < ApplicationRecord validates :tag, presence: true validates :language, presence: true, inclusion: { in: %w[en cy] } + + def self.latest_live_or_archived(form_id:, language:) + FormDocument.where(form_id: form_id, language: language) + .where.not(version: nil) + .order(version: :desc) + .first + end end diff --git a/spec/models/form_document_spec.rb b/spec/models/form_document_spec.rb index 3948058e88..015a1f7d02 100644 --- a/spec/models/form_document_spec.rb +++ b/spec/models/form_document_spec.rb @@ -65,4 +65,71 @@ form = create(:form) # also creates a draft form document expect { create(:form_document, :draft, form: form, language: "cy") }.not_to raise_error end + + describe ".latest_live_or_archived" do + let(:form) { create :form } + + context "when the form only has a draft form document" do + it "returns nil" do + expect(form.draft_form_document).to be_present + expect(described_class.latest_live_or_archived(form_id: form.id, language: "en")).to be_nil + end + end + + context "when there is one form document with a version" do + let!(:form_document) { create :form_document, :live, form:, language: "en", version: 1 } + + it "returns the form document" do + expect(described_class.latest_live_or_archived(form_id: form.id, language: "en")).to eq(form_document) + end + + it "returns nil when no form document for the language exists" do + expect(described_class.latest_live_or_archived(form_id: form.id, language: "cy")).to be_nil + end + end + + context "when there are multiple form documents with different versions" do + let(:latest_document) { create :form_document, :archived, form:, language: "en", version: 3 } + + before do + create :form_document, :live, form:, language: "en", version: 1 + latest_document + create :form_document, :live, form:, language: "en", version: 2 + end + + it "returns the latest form document with the highest version" do + expect(described_class.latest_live_or_archived(form_id: form.id, language: "en")).to eq(latest_document) + end + end + + context "when there are form documents for different languages" do + let(:latest_english_document) { create :form_document, form:, language: "en", version: 2 } + let(:latest_welsh_document) { create :form_document, form:, language: "cy", version: 1 } + + before do + create :form_document, form:, language: "en", version: 1 + latest_english_document + latest_welsh_document + end + + it "returns the latest English document regardless of Welsh versions" do + expect(described_class.latest_live_or_archived(form_id: form.id, language: "en")).to eq(latest_english_document) + end + + it "returns the latest Welsh document regardless of English versions" do + expect(described_class.latest_live_or_archived(form_id: form.id, language: "cy")).to eq(latest_welsh_document) + end + end + + context "when documents from other forms exist" do + let(:other_form) { create :form } + let!(:other_form_document) { create :form_document, :live, form: other_form, language: "en", version: 2 } + let!(:this_form_document) { create :form_document, :live, form:, language: "en", version: 1 } + + it "only returns documents from the current form" do + expect(described_class.latest_live_or_archived(form_id: form.id, language: "en")).to eq(this_form_document) + expect(described_class.latest_live_or_archived(form_id: other_form.id, language: "en")).to eq(other_form_document) + end + end + end end From bacc7675ac12fbfc645053028a5ba14e89d20e14 Mon Sep 17 00:00:00 2001 From: Stephen Daly Date: Wed, 12 Aug 2026 16:32:15 +0100 Subject: [PATCH 2/3] Update forms API to return latest version We've made it possible for multiple form documents with the same language and tag to exist with different version numbers. Update the forms API to return the latest version of the form document with the given language, if the tag matches the requested tag. If the draft tag is requested, the only draft form document for the language will be returned. This means that the behaviour of the API remains unchanged externally once we are retaining form documents for previous versions. --- .../api/form_documents_controller.rb | 10 ++- .../api/form_documents_controller_spec.rb | 64 ++++++++++++------- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/app/controllers/api/form_documents_controller.rb b/app/controllers/api/form_documents_controller.rb index 421f85cd95..1e61b96f33 100644 --- a/app/controllers/api/form_documents_controller.rb +++ b/app/controllers/api/form_documents_controller.rb @@ -10,7 +10,15 @@ def group private def form_document - @form_document ||= FormDocument.find_by!(form_document_params) + form_id, tag, language = form_document_params.values_at(:form_id, :tag, :language) + + return FormDocument.find_by!(form_id:, tag:, language:) if tag == "draft" + + latest_form_document = FormDocument.latest_live_or_archived(form_id:, language:) + + raise NotFoundError unless latest_form_document&.tag == tag + + latest_form_document end def form_document_params diff --git a/spec/requests/api/form_documents_controller_spec.rb b/spec/requests/api/form_documents_controller_spec.rb index d616e477da..6fcc877fd1 100644 --- a/spec/requests/api/form_documents_controller_spec.rb +++ b/spec/requests/api/form_documents_controller_spec.rb @@ -34,51 +34,67 @@ end context "when the tag is live" do - let(:live_form_name) { "Live form" } - let(:form) { create(:form, :live, name: live_form_name) } + let(:form) { create(:form, :live) } before do - # change the form object so we can be sure we're returning the live form document - form.name = "Draft form" - form.save! - - get "/api/v2/forms/#{form.id}/live", headers: + create :form_document, :live, form: form, version: 2, content: { name: "v2 form" } + create :form_document, :live, form: form, version: 3, content: { name: "v3 form" } end it "returns http success" do + get("/api/v2/forms/#{form.id}/live", headers:) expect(response).to have_http_status(:success) end - it "returns the live form document" do + it "returns the latest live form document" do + get("/api/v2/forms/#{form.id}/live", headers:) expect(response.parsed_body).to include({ - form_id: form.id.to_s, - name: live_form_name, + name: "v3 form", }) end + + context "and the most recent version is archived" do + before do + create :form_document, :archived, form: form, version: 4 + end + + it "returns http not found" do + get("/api/v2/forms/#{form.id}/live", headers:) + expect(response).to have_http_status(:not_found) + end + end end context "when the tag is archived" do - let(:archived_form_name) { "Archived form" } - let(:form) { create(:form, :archived, name: archived_form_name) } + let(:form) { create(:form, :archived) } before do - # change the form object so we can be sure we're returning the archived form document - form.name = "Draft form" - form.save! - - get "/api/v2/forms/#{form.id}/archived" + create :form_document, :archived, form: form, version: 2, content: { name: "v2 form" } + create :form_document, :archived, form: form, version: 3, content: { name: "v3 form" } end it "returns http success" do + get("/api/v2/forms/#{form.id}/archived") expect(response).to have_http_status(:success) end it "returns the archived form document" do + get("/api/v2/forms/#{form.id}/archived") expect(response.parsed_body).to include({ - form_id: form.id.to_s, - name: archived_form_name, + name: "v3 form", }) end + + context "and the most recent version is live" do + before do + create :form_document, :live, form: form, version: 4 + end + + it "returns http not found" do + get("/api/v2/forms/#{form.id}/archived") + expect(response).to have_http_status(:not_found) + end + end end end @@ -123,14 +139,16 @@ let(:form) { create :form } before do - create :form_document, :live, form: form, language: "en", content: { form_id: form.id.to_s, language: "en" } - create :form_document, :live, form: form, language: "cy", content: { form_id: form.id.to_s, language: "cy" } + create :form_document, :live, form: form, language: "en", version: 1 + create :form_document, :live, form: form, language: "en", version: 2, content: { name: "Live form v2", language: "en" } + create :form_document, :live, form: form, language: "cy", version: 1 + create :form_document, :live, form: form, language: "cy", version: 2, content: { name: "Welsh live form v2", language: "cy" } end it "when not given a language, defaults to english returns the live form document in english" do get("/api/v2/forms/#{form.id}/live", headers:) expect(response.parsed_body).to include({ - form_id: form.id.to_s, + name: "Live form v2", language: "en", }) end @@ -138,7 +156,7 @@ it "when given welsh param returns the live form document in welsh" do get("/api/v2/forms/#{form.id}/live?language=cy", headers:) expect(response.parsed_body).to include({ - form_id: form.id.to_s, + name: "Welsh live form v2", language: "cy", }) end From efec32b93705e3966b36f55aaa58c35a3ea23cf1 Mon Sep 17 00:00:00 2001 From: Stephen Daly Date: Thu, 13 Aug 2026 12:04:12 +0100 Subject: [PATCH 3/3] Log the form document version returned by the API --- app/controllers/api/form_documents_controller.rb | 2 ++ app/models/current_logging_attributes.rb | 1 + spec/requests/api/form_documents_controller_spec.rb | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/app/controllers/api/form_documents_controller.rb b/app/controllers/api/form_documents_controller.rb index 1e61b96f33..82fc2260ea 100644 --- a/app/controllers/api/form_documents_controller.rb +++ b/app/controllers/api/form_documents_controller.rb @@ -18,6 +18,8 @@ def form_document raise NotFoundError unless latest_form_document&.tag == tag + CurrentLoggingAttributes.form_document_version = latest_form_document&.version + latest_form_document end diff --git a/app/models/current_logging_attributes.rb b/app/models/current_logging_attributes.rb index 91724511b6..e335f54b02 100644 --- a/app/models/current_logging_attributes.rb +++ b/app/models/current_logging_attributes.rb @@ -11,6 +11,7 @@ class CurrentLoggingAttributes < ActiveSupport::CurrentAttributes :acting_as_user_email, :acting_as_user_organisation_slug, :form_id, + :form_document_version, :page_id, :answer_type, :auth0_session_id, diff --git a/spec/requests/api/form_documents_controller_spec.rb b/spec/requests/api/form_documents_controller_spec.rb index 6fcc877fd1..c970a87a4f 100644 --- a/spec/requests/api/form_documents_controller_spec.rb +++ b/spec/requests/api/form_documents_controller_spec.rb @@ -53,6 +53,11 @@ }) end + it "logs the returned form document version", :capture_logging do + get("/api/v2/forms/#{form.id}/live", headers:) + expect(log_line["form_document_version"]).to eq 3 + end + context "and the most recent version is archived" do before do create :form_document, :archived, form: form, version: 4