Repository: decidim/decidim (Ruby)

Issue:
Allow editing the vote for the last question in a question by question election
### Describe the bug

This is point 5 of #15592 

Given a **question by question** election, when the user is voting questions, it can edit the previous ones (if enabled) except the last one.
This is because the last screen presented is the one saying "you have finished voting" and only the "exit the voting booth" button appears.

We should add a "edit the previous question" button here, exactly as the rest of the pages when voting other questions.

### To Reproduce

1. Create an "question by question" election with at least 2 questions. activate all questions for voting.
2. Go to vote, vote on all questions, see that there is a "edit previous question" in each page except the last one

### Expected behavior

The last sumary page should allow editing the last vote if the question is still available.

### Screenshots

<img width="1381" height="678" alt="Image" src="https://github.com/user-attachments/assets/52e8b376-300d-4e98-a497-2976d183a153" />

### Stacktrace

_No response_

### Extra data

- Device:
- Device OS:
- Browser:
- Decidim Version:
- Decidim installation:


### Additional context

_No response_



Fix (patch):
diff --git a/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb b/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb
index 08cf7b7c030c8..6dd6b9f38c4a7 100644
--- a/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb
+++ b/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb
@@ -49,10 +49,16 @@ def create
 
       # Shows the receipt page
       def receipt
+        if params[:exit].present?
+          votes_buffer.clear
+          session_attributes.clear
+          return redirect_to(exit_path)
+        end
+
         enforce_permission_to(:create, :vote, election:)
 
-        votes_buffer.clear
-        session_attributes.clear
+        votes_buffer.clear unless election.per_question?
+
         return redirect_to(exit_path) unless election.votes.exists?(voter_uid: session[:voter_uid])
 
         render "decidim/elections/votes/receipt"
diff --git a/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb b/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb
index cceaddb0fc58d..ebd2bb2d7681f 100644
--- a/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb
+++ b/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb
@@ -29,6 +29,7 @@ def update
         enforce_permission_to(:create, :vote, election:)
 
         response_ids = params.dig(:response, question.id.to_s)
+        requeue_following_questions
         votes_buffer[question.id.to_s] = response_ids
         CastVotes.call(election, { question.id.to_s => response_ids }, voter_uid) do
           on(:ok) do
@@ -84,6 +85,13 @@ def next_vote_step_action
 
         { action: :show, id: next_question }
       end
+
+      def requeue_following_questions
+        election.questions
+                .where("position > ?", question.position)
+                .pluck(:id)
+                .each { |id| votes_buffer.delete(id.to_s) }
+      end
     end
   end
 end
diff --git a/decidim-elections/app/views/decidim/elections/votes/receipt.html.erb b/decidim-elections/app/views/decidim/elections/votes/receipt.html.erb
index 7bdeb45e33781..9d240b5609fa5 100644
--- a/decidim-elections/app/views/decidim/elections/votes/receipt.html.erb
+++ b/decidim-elections/app/views/decidim/elections/votes/receipt.html.erb
@@ -4,5 +4,22 @@
     <h2 class="h2 text-center mb-6"><%= t(".title") %></h2>
     <p class="text-center"><%= t(".description") %></p>
   </div>
-  <%= link_to t(".exit_button"), exit_path, class: "button button__secondary button__lg w-full mt-12" %>
+
+  <% editable_question = election.per_question? ? election.questions.enabled.unpublished_results.last : nil %>
+  <% exit_button_path = election.per_question? ? url_for(action: :receipt, exit: true) : exit_path %>
+  <% if editable_question.present? %>
+    <div class="vote-navigation w-full flex justify-between mt-8">
+      <%= link_to election_per_question_vote_path(election_id: election, id: editable_question),
+                  class: "button button__lg button__transparent-secondary" do %>
+        <%= icon "edit-line", class: "fill-current mr-2" %>
+        <%= t(".edit_vote") %>
+      <% end %>
+
+      <div class="ml-auto">
+        <%= link_to t(".exit_button"), exit_button_path, class: "button button__lg button__secondary" %>
+      </div>
+    </div>
+  <% else %>
+    <%= link_to t(".exit_button"), exit_button_path, class: "button button__secondary button__lg w-full mt-12" %>
+  <% end %>
 </div>
diff --git a/decidim-elections/config/locales/en.yml b/decidim-elections/config/locales/en.yml
index a6d488e49d941..e25d269b09364 100644
--- a/decidim-elections/config/locales/en.yml
+++ b/decidim-elections/config/locales/en.yml
@@ -316,7 +316,8 @@ en:
           max_choices_exceeded: You cannot select more than %{max} options. Please go back and adjust your selection.
           next: Next
         receipt:
-          description: Yo can vote again at any time while the voting period is open. Your previous vote will be overwritten by the new one.
+          description: You can vote again at any time while the voting period is open. Your previous vote will be overwritten by the new one.
+          edit_vote: Edit your vote
           exit_button: Exit the voting booth
           title: Your votes have been cast successfully
     metadata:
diff --git a/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb
index a16c780fd1a8c..abab9d9912189 100644
--- a/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb
+++ b/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb
@@ -29,6 +29,7 @@ module Elections
         allow(controller).to receive(:current_participatory_space).and_return(component.participatory_space)
         allow(controller).to receive(:current_component).and_return(component)
         allow(controller).to receive(:election_vote_path).and_return(election_vote_path)
+        allow(controller).to receive(:second_election_vote_path).and_return(second_election_vote_path)
         allow(controller).to receive(:new_election_vote_path).and_return(new_election_vote_path)
         allow(controller).to receive(:election_path).and_return(election_path)
         allow(controller).to receive(:waiting_election_votes_path).and_return(waiting_election_votes_path)
@@ -118,14 +119,23 @@ module Elections
           end
 
           it "casts the votes and redirects to the receipt page if successful" do
-            # ensure there are no pending votes
-            allow(controller).to receive(:votes_buffer).and_return({ question.id.to_s => [question.response_options.first.id], second_question.id.to_s => [second_question.response_options.first.id] })
+            session[:votes_buffer] = { question.id.to_s => [question.response_options.first.id.to_s], second_question.id.to_s => [second_question.response_options.first.id.to_s] }
 
             expect(controller).to receive(:redirect_to).with(action: :receipt)
-            patch :update, params: params.merge(id: question.id, response: { question.id.to_s => [question.response_options.first.id] })
+            patch :update, params: params.merge(id: second_question.id, response: { second_question.id.to_s => [second_question.response_options.first.id] })
             expect(session[:voter_uid]).to eq(user.to_global_id.to_s)
             expect(flash[:notice]).to eq(I18n.t("votes.cast.success", scope: "decidim.elections"))
           end
+
+          it "clears subsequent questions from buffer when updating a previous question" do
+            session[:votes_buffer] = { question.id.to_s => [question.response_options.first.id.to_s], second_question.id.to_s => [second_question.response_options.first.id.to_s] }
+
+            expect(controller).to receive(:redirect_to).with(action: :show, id: second_question)
+            patch :update, params: params.merge(id: question.id, response: { question.id.to_s => [question.response_options.second.id] })
+
+            expect(session[:votes_buffer]).not_to have_key(second_question.id.to_s)
+            expect(session[:votes_buffer][question.id.to_s]).to eq([question.response_options.second.id.to_s])
+          end
         end
       end
 
@@ -217,13 +227,20 @@ module Elections
 
               it_behaves_like "a redirect to the waiting room", :receipt
 
-              it "renders the receipt page" do
-                expect(controller.send(:votes_buffer)).to receive(:clear)
-                expect(controller.send(:session_attributes)).to receive(:clear)
+              it "renders the receipt page without clearing session" do
+                expect(controller.send(:votes_buffer)).not_to receive(:clear)
+                expect(controller.send(:session_attributes)).not_to receive(:clear)
                 get :receipt, params: params
                 expect(response).to have_http_status(:ok)
                 expect(subject).to render_template(:receipt)
               end
+
+              it "clears session when exit param is present" do
+                expect(controller.send(:votes_buffer)).to receive(:clear)
+                expect(controller.send(:session_attributes)).to receive(:clear)
+                get :receipt, params: params.merge(exit: true)
+                expect(response).to redirect_to(election_path)
+              end
             end
           end
         end
diff --git a/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb
index 7cab91d3c4936..c086c74b3366b 100644
--- a/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb
+++ b/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb
@@ -235,13 +235,20 @@ module Elections
               create(:election_vote, voter_uid: session[:voter_uid], question: question, response_option: question.response_options.first)
             end
 
-            it "renders the receipt page" do
+            it "renders the receipt page and clears votes buffer" do
               expect(controller.send(:votes_buffer)).to receive(:clear)
-              expect(controller.send(:session_attributes)).to receive(:clear)
+              expect(controller.send(:session_attributes)).not_to receive(:clear)
               get :receipt, params: params
               expect(response).to have_http_status(:ok)
               expect(subject).to render_template(:receipt)
             end
+
+            it "clears session when exit param is present" do
+              expect(controller.send(:votes_buffer)).to receive(:clear)
+              expect(controller.send(:session_attributes)).to receive(:clear)
+              get :receipt, params: params.merge(exit: true)
+              expect(response).to redirect_to(election_path)
+            end
           end
         end
       end
diff --git a/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb b/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb
index 7be60f6e774b7..3b88aed74897a 100644
--- a/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb
+++ b/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb
@@ -73,6 +73,19 @@ def election_vote_path(question)
     it_behaves_like "a per question votable election with already voted questions"
   end
 
+  context "when editing votes from receipt page" do
+    let!(:question1) { create(:election_question, :with_response_options, :voting_enabled, skip_injection: true, question_type: "single_option", election:, position: 1) }
+    let!(:question2) { create(:election_question, :with_response_options, :voting_enabled, election:, skip_injection: true, position: 2) }
+
+    before do
+      login_as user, scope: :user
+      visit election_path
+    end
+
+    it_behaves_like "a per question votable election with edit from receipt"
+    it_behaves_like "a per question votable election with edit from receipt when all questions enabled"
+  end
+
   context "when the election is real time" do
     let(:election) { create(:election, :published, :ongoing, :with_internal_users_census, :real_time, :with_questions) }