Files
nairobi/app/controllers/officing/voters_controller.rb
Javi Martín 6da53b5716 Add unique index to poll voters table
Note that Rails 7.1 changes `find_or_create_by!` so it calls
`create_or_find_by!` when no record is found, meaning we'll rarely get
`RecordNotUnique` exceptions when using this method during a race
condition.

Adding this index means we need to remove the uniqueness validation.
According to the `create_or_find_by` documentation [1]:

> Columns with unique database constraints should not have uniqueness
> validations defined, otherwise create will fail due to validation
> errors and find_by will never be called.

We're adding a test that checks what happens when using
`create_or_find_by!`.

Note that we're creating voters combining `create_with` with
`find_or_create_by!`. Using `find_or_create_by!(...)` with all
attributes (including non-key ones like `origin`) fails when a voter
already exists with different values, e.g. an existing `origin: "web"`
and an incoming `"booth"`. In this situation the existing record is not
matched and the unique index raises an exception.

`create_with(...).find_or_create_by!(user: ..., poll: ...)` searches by
the unique key only and applies the extra attributes only on creation.
Existing voters are returned unchanged, which is the intended behavior.

For the `take_votes_from` method, we're handling a (highly unlikely, but
theoretically possible) scenario where a user votes at the same time as
taking voters from another user. For that, we're doing something similar
to what `create_or_find_by!` does: we're updating the `user_id` column
inside a new transaction (using a new transactions avoids a
`PG::InFailedSqlTransaction` exception when there are duplicate
records), and deleting the existing voter when we get a
`RecordNotUnique` exception.

On `Poll::WebVote` we're simply raising an exception when there's
already a user who's voted via booth, because the `Poll::WebVote#update`
method should never be called in this case.

We still need to use `with_lock` in `Poll::WebVote`, but not due to
duplicate voters (`find_or_create_by!` method will now handle the unique
record scenario, even in the case of simultaneous transactions), but
because we use a uniqueness validation in `Poll::Answer`; this
validation would cause an error in simultaneous transactions.

[1] https://api.rubyonrails.org/v7.1/classes/ActiveRecord/Relation.html#method-i-create_or_find_by
2025-08-28 14:42:30 +02:00

41 lines
1.2 KiB
Ruby

class Officing::VotersController < Officing::BaseController
respond_to :html, :js
before_action :load_officer_assignment
before_action :verify_officer_assignment
before_action :verify_booth
def new
@user = User.find(params[:id])
@polls = current_booth.polls.current
end
def create
@poll = Poll.find(voter_params[:poll_id])
@user = User.find(voter_params[:user_id])
@voter = Poll::Voter.create_with(
document_type: @user.document_type,
document_number: @user.document_number,
origin: "booth",
officer: current_user.poll_officer,
booth_assignment: current_booth.booth_assignments.find_by(poll: @poll),
officer_assignment: officer_assignment(@poll)
).find_or_create_by!(user: @user, poll: @poll)
end
private
def voter_params
params.require(:voter).permit(:poll_id, :user_id)
end
def officer_assignment(poll)
Poll::OfficerAssignment.by_officer(current_user.poll_officer)
.by_poll(poll)
.by_booth(current_booth)
.by_date(Date.current)
.find_by(final: false)
end
end