Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto Accept PR 2: Frontend & Waiting List Integration #10416

Open
wants to merge 79 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
dc1a8c5
added test to start the PR
dunkOnIT Dec 7, 2024
6dab06f
started adding db fields
dunkOnIT Dec 7, 2024
7a90640
added competition fields and model tests
dunkOnIT Dec 8, 2024
fac6289
made auto accept fields available to competition form
dunkOnIT Dec 8, 2024
6c24925
added auto_accept fields to competition form
dunkOnIT Dec 8, 2024
4fa9ddc
rubocop
dunkOnIT Dec 8, 2024
8f205ab
very basic auto-accept added
dunkOnIT Dec 8, 2024
9f6155c
added a few more tests
dunkOnIT Dec 9, 2024
39a032c
more auto accept enforcement tests
dunkOnIT Dec 10, 2024
735f1d1
added comp full validation and auto_accept tests
dunkOnIT Dec 10, 2024
c6dce04
rubocop
dunkOnIT Dec 10, 2024
f4b4e6b
merged main
dunkOnIT Dec 10, 2024
16db8bf
added test for not bypassing series reg limit
dunkOnIT Dec 10, 2024
a09d1bb
added check for competing_status = pending
dunkOnIT Dec 10, 2024
8c3cdaf
refactored tests
dunkOnIT Dec 10, 2024
557a176
fixing test failures
dunkOnIT Dec 10, 2024
cb2e396
added integration test
dunkOnIT Dec 10, 2024
b8fd76e
fixed tests
dunkOnIT Dec 11, 2024
9e62947
removed fields from frontend
dunkOnIT Dec 11, 2024
5276b09
creating a diff
dunkOnIT Dec 11, 2024
d1e8d7e
Merge branch 'v3/auto-accept' into v3/auto-accept-2
dunkOnIT Dec 11, 2024
aa59c22
removed unnecessary comment
dunkOnIT Dec 11, 2024
0bc1dd2
added auto_accept from waiting list
dunkOnIT Dec 12, 2024
15ee692
added auto-accept onto waiting list
dunkOnIT Dec 13, 2024
4b7156e
started adding bulk auto accept tests
dunkOnIT Dec 13, 2024
3204be3
bulk accept tests before auto_accept method changes
dunkOnIT Dec 17, 2024
a81ec1d
many changes - waiting list, log validation errors
dunkOnIT Dec 19, 2024
f81102d
bulk auto accept w/ competitor limit tests
dunkOnIT Dec 19, 2024
9e7f7cb
bulk auto-accept tests finished
dunkOnIT Dec 19, 2024
0e1f481
rubocop
dunkOnIT Dec 19, 2024
30ece91
removed unnecessary safe accessor
dunkOnIT Dec 19, 2024
a695518
removed unnecessary comments from migration
dunkOnIT Dec 19, 2024
3f24c5b
re-enabled random test order
dunkOnIT Dec 19, 2024
e1b1c2a
waiting list fix
dunkOnIT Dec 19, 2024
32a5f4c
adding buttons and routes
dunkOnIT Jan 12, 2025
9b46a44
added buttons and played around on local
dunkOnIT Jan 13, 2025
8d3f145
basic auto accept and enable checks
dunkOnIT Jan 14, 2025
c8bd84c
added disable route
dunkOnIT Jan 14, 2025
d2cf7f9
got disable auto accept button working
dunkOnIT Jan 14, 2025
6bfb721
added mutation for disable auto accept
dunkOnIT Jan 15, 2025
4a0b8ca
added responsive functionality to button
dunkOnIT Jan 15, 2025
d4ec0a7
make auto accept button disappear when disabled
dunkOnIT Jan 16, 2025
33eceea
removed bulk auto accept stuff
dunkOnIT Jan 16, 2025
e4c5d90
diff check
dunkOnIT Jan 16, 2025
0d5c0ac
rubocop
dunkOnIT Jan 16, 2025
3e782eb
Merge branch 'main' into v3/auto-accept-2
dunkOnIT Jan 16, 2025
033fb26
fixed eslint
dunkOnIT Jan 16, 2025
4b603bc
Merge branch 'v3/auto-accept-2' of github.com:dunkOnIT/worldcubeassoc…
dunkOnIT Jan 16, 2025
6f5aba4
removed fragment and unused error object
dunkOnIT Jan 16, 2025
73b21f8
fixed test
dunkOnIT Jan 18, 2025
30b8247
Merge branch 'main' into v3/auto-accept-2
dunkOnIT Jan 27, 2025
c5ff83a
linter fixes
dunkOnIT Jan 27, 2025
c07d866
linter fixes
dunkOnIT Jan 27, 2025
f4b953f
pleasing the linter gods
dunkOnIT Jan 27, 2025
1c407e5
the linter gods are fickle
dunkOnIT Jan 27, 2025
050f2ec
switched to fetching competitionInfo
dunkOnIT Jan 30, 2025
d496cf9
removed typo and changed all competitionInfo.id instances to competit…
dunkOnIT Jan 30, 2025
fc00c1b
passing in initial competitionInfo as a prop
dunkOnIT Jan 30, 2025
89b972f
refactored parent component into difft file
dunkOnIT Jan 31, 2025
bb2b3f4
disable auto accept button working properly
dunkOnIT Feb 1, 2025
0f311fa
eslint fixes
dunkOnIT Feb 1, 2025
1164b51
eslint changes 2
dunkOnIT Feb 1, 2025
b38de16
eslint 3
dunkOnIT Feb 1, 2025
3eff450
fixed oopsie on edit.html.erb
dunkOnIT Feb 1, 2025
eb680a6
fixed test failure
dunkOnIT Feb 1, 2025
84e7bce
changed i18n key invocation
dunkOnIT Feb 1, 2025
4b35332
trying to get eslint to stop flagging the backquotes in i18n key
dunkOnIT Feb 1, 2025
128aca7
removed aparently unused key
dunkOnIT Feb 1, 2025
b13c1af
linter fix
dunkOnIT Feb 3, 2025
f40897d
going to lose my mind
dunkOnIT Feb 3, 2025
89125f7
remove unused key
dunkOnIT Feb 3, 2025
07cf6f9
diff review
dunkOnIT Feb 3, 2025
9de116f
added back competition info error
dunkOnIT Feb 4, 2025
23c434a
lint fix
dunkOnIT Feb 4, 2025
95d1676
linter fix
dunkOnIT Feb 4, 2025
566dfe1
Merge branch 'main' into v3/auto-accept-2
dunkOnIT Feb 4, 2025
c6ed98b
linter
dunkOnIT Feb 4, 2025
785d598
new yarn lock
dunkOnIT Feb 4, 2025
7f2a1f8
comma dangle
dunkOnIT Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/controllers/api/v0/competitions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,14 @@ def update_wcif
}
end

def disable_auto_accept
competition = competition_from_params
require_can_manage!(competition)

competition.update!(auto_accept_registrations: false)
render json: { status: 'success' }
end

private def competition_from_params(associations: {})
id = params[:competition_id] || params[:id]
competition = Competition.includes(associations).find_by_id(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Api::V1::Registrations::RegistrationsController < Api::V1::ApiController
# before_actions are triggered in the order they are defined
before_action :validate_create_request, only: [:create]
before_action :validate_show_registration, only: [:show]
before_action :validate_list_admin, only: [:list_admin]
before_action :validate_admin_action, only: [:list_admin]
before_action :validate_update_request, only: [:update]
before_action :validate_bulk_update_request, only: [:bulk_update]
before_action :validate_payment_ticket_request, only: [:payment_ticket]
Expand Down Expand Up @@ -103,9 +103,8 @@ def list
end

# To list Registrations in the admin view you need to be able to administer the competition
def validate_list_admin
def validate_admin_action
competition_id = list_params
# TODO: Do we set this as an instance variable here so we can use it below?
@competition = Competition.find(competition_id)
unless @current_user.can_manage_competition?(@competition)
render_error(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS)
Expand Down
40 changes: 39 additions & 1 deletion app/models/competition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ class Competition < ApplicationRecord
event_change_deadline_date
competition_series_id
registration_version
auto_accept_registrations
auto_accept_disable_threshold
).freeze
VALID_NAME_RE = /\A([-&.:' [:alnum:]]+) (\d{4})\z/
VALID_ID_RE = /\A[a-zA-Z0-9]+\Z/
Expand Down Expand Up @@ -354,6 +356,25 @@ def advancement_condition_must_be_present_for_all_non_final_rounds
end
end

validate :auto_accept_validations
private def auto_accept_validations
errors.add(:auto_accept_registrations, I18n.t('competitions.errors.must_use_wca_registration')) if
auto_accept_registrations && !use_wca_registration

errors.add(:auto_accept_registrations, I18n.t('competitions.errors.auto_accept_limit')) if
auto_accept_disable_threshold > 0 &&
competitor_limit.present? &&
auto_accept_disable_threshold >= competitor_limit

errors.add(:auto_accept_registrations, I18n.t('competitions.errors.auto_accept_not_negative')) if auto_accept_disable_threshold < 0

if auto_accept_registrations_changed?
errors.add(:auto_accept_registrations, I18n.t('competitions.errors.auto_accept_accept_paid_pending')) if registrations.pending.with_payments.count > 0
errors.add(:auto_accept_registrations, I18n.t('competitions.errors.auto_accept_accept_waitlisted')) if
registrations.waitlisted.count > 0 && !accepted_full?
end
end

def has_any_round_per_event?
competition_events.map(&:rounds).none?(&:empty?)
end
Expand Down Expand Up @@ -453,11 +474,16 @@ def confirmed_or_visible?
self.confirmed? || self.showAtAll
end

# TODO: Consider refactoring this once auto-accept is implemented
def registration_full?
competitor_count = registrations.accepted_and_paid_pending_count
competitor_limit_enabled? && competitor_count >= competitor_limit
end

def accepted_full?
competitor_limit_enabled? && registrations.competing_status_accepted.count >= competitor_limit
end

def number_of_bookmarks
bookmarked_users.count
end
Expand Down Expand Up @@ -1877,7 +1903,7 @@ def to_competition_info
base_entry_fee_lowest_denomination currency_code allow_registration_edits allow_registration_self_delete_after_acceptance
allow_registration_without_qualification refund_policy_percent use_wca_registration guests_per_registration_limit venue contact
force_comment_in_registration use_wca_registration external_registration_page guests_entry_fee_lowest_denomination guest_entry_status
information events_per_registration_limit guests_enabled],
information events_per_registration_limit guests_enabled auto_accept_registrations auto_accept_disable_threshold],
methods: %w[url website short_name city venue_address venue_details latitude_degrees longitude_degrees country_iso2 event_ids registration_currently_open?
main_event_id number_of_bookmarks using_payment_integrations? uses_qualification? uses_cutoff? competition_series_ids registration_full?
part_of_competition_series?],
Expand Down Expand Up @@ -2427,6 +2453,8 @@ def to_form_data
"admin" => {
"isConfirmed" => confirmed?,
"isVisible" => showAtAll?,
"autoAcceptEnabled" => auto_accept_registrations,
"autoAcceptDisableThreshold" => auto_accept_disable_threshold,
},
"cloning" => {
"fromId" => being_cloned_from_id,
Expand Down Expand Up @@ -2528,6 +2556,8 @@ def form_errors
"admin" => {
"isConfirmed" => errors[:confirmed_at],
"isVisible" => errors[:showAtAll],
"autoAcceptEnabled" => errors[:auto_accept_registrations],
"autoAcceptDisableThreshold" => errors[:auto_accept_disable_threshold],
},
"cloning" => {
"fromId" => errors[:being_cloned_from_id],
Expand Down Expand Up @@ -2633,6 +2663,8 @@ def self.form_data_to_attributes(form_data)
showAtAll: form_data.dig('admin', 'isVisible'),
being_cloned_from_id: form_data.dig('cloning', 'fromId'),
clone_tabs: form_data.dig('cloning', 'cloneTabs'),
auto_accept_registrations: form_data.dig('admin', 'autoAcceptEnabled'),
auto_accept_disable_threshold: form_data.dig('admin', 'autoAcceptDisableThreshold'),
}
end

Expand Down Expand Up @@ -2816,6 +2848,8 @@ def self.form_data_json_schema
"guestsPerRegistration" => { "type" => ["integer", "null"] },
"extraRequirements" => { "type" => ["string", "null"] },
"forceComment" => { "type" => ["boolean", "null"] },
"autoAcceptEnabled" => { "type" => ["boolean", "null"] },
"autoAcceptDisableThreshold" => { "type" => ["integer", "null"] },
},
},
"eventRestrictions" => {
Expand Down Expand Up @@ -2872,4 +2906,8 @@ def self.form_data_json_schema
},
}
end

def auto_accept_threshold_reached?
auto_accept_disable_threshold > 0 && auto_accept_disable_threshold <= registrations.competing_status_accepted.count
end
end
62 changes: 60 additions & 2 deletions app/models/registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ class Registration < ApplicationRecord

after_save :mark_registration_processing_as_done

after_update :waiting_list_auto_accept_check

private def mark_registration_processing_as_done
Rails.cache.delete(CacheAccess.registration_processing_cache_key(competition_id, user_id))
end

def update_lanes!(params, acting_user)
Registrations::Lanes::Competing.update!(params, self.competition, acting_user.id)
def update_lanes!(params, acting_user_id)
Registrations::Lanes::Competing.update!(params, self.competition, acting_user_id)
end

def guest_limit
Expand Down Expand Up @@ -335,6 +337,14 @@ def self.accepted_and_paid_pending_count
end
end

validate :does_not_exceed_competitor_limit
private def does_not_exceed_competitor_limit
return unless competition&.competitor_limit.present?
return unless competing_status == Registrations::Helper::STATUS_ACCEPTED
errors.add(:competitor_limit, I18n.t('registrations.errors.competitor_limit_reached')) if
competition.registrations.competing_status_accepted.count >= competition.competitor_limit
end

# TODO: V3-REG cleanup. All these Validations can be used instead of the registration_checker checks
validate :cannot_be_undeleted_when_banned, if: :competing_status_changed?
private def cannot_be_undeleted_when_banned
Expand Down Expand Up @@ -428,4 +438,52 @@ def series_registration_info
def serializable_hash(options = nil)
super(DEFAULT_SERIALIZE_OPTIONS.merge(options || {}))
end

def auto_accept
return log_error('Auto-accept is not enabled for this competition.') unless competition.auto_accept_registrations
return log_error('Can only auto-accept pending registrations or first position on waiting list') unless
competing_status_pending? || (competing_status_waiting_list? && waiting_list_position == 1)
return log_error("Competition has reached auto_accept_disable_threshold of #{competition.auto_accept_disable_threshold} registrations") if
competition.auto_accept_threshold_reached?
return log_error('Competitor still has outstanding registration fees') if outstanding_entry_fees > 0
return log_error('Cant auto-accept while registration is not open') if !competition.registration_currently_open?

if competition.accepted_full? && competing_status_pending?
update_lanes!(
{ user_id: user_id, competing: { status: Registrations::Helper::STATUS_WAITING_LIST } }.with_indifferent_access,
'Auto-accept',
)
else
update_lanes!(
{ user_id: user_id, competing: { status: Registrations::Helper::STATUS_ACCEPTED } }.with_indifferent_access,
'Auto-accept',
)
end
rescue ActiveRecord::RecordInvalid => e
log_error("Auto accept error for registration #{id}: #{e}")
end

private def log_error(error)
Rails.logger.error(error)
false
end

private def waiting_list_auto_accept_check
changed_from_accepted = saved_change_to_competing_status? && saved_change_to_competing_status.first == Registrations::Helper::STATUS_ACCEPTED
return unless changed_from_accepted

waiting_list_leader_id = competition.waiting_list&.entries&.first

if changed_from_accepted && waiting_list_leader_id.present?
Registration.find(waiting_list_leader_id).auto_accept
end
end

def last_positive_payment
registration_payments
.where('amount_lowest_denomination > 0')
.order(updated_at: :desc)
.limit(1)
.first
end
end
6 changes: 6 additions & 0 deletions app/models/registration_payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class RegistrationPayment < ApplicationRecord
belongs_to :refunded_registration_payment, class_name: 'RegistrationPayment', optional: true
has_many :refunding_registration_payments, class_name: 'RegistrationPayment', inverse_of: :refunded_registration_payment, foreign_key: :refunded_registration_payment_id, dependent: :destroy

after_create :attempt_auto_accept

monetize :amount_lowest_denomination,
as: "amount",
allow_nil: true,
Expand All @@ -26,4 +28,8 @@ def payment_status
receipt.determine_wca_status
end
end

private def attempt_auto_accept
registration.auto_accept
end
end
2 changes: 2 additions & 0 deletions app/models/waiting_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ def remove(entry_id)
end

def add(entry_id)
return if entries.include?(entry_id)

if entries.nil?
update_column :entries, [entry_id]
else
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import React from 'react';
import { InputBoolean } from '../../wca/FormBuilder/input/FormInputs';
import { InputBoolean, InputBooleanSelect, InputNumber } from '../../wca/FormBuilder/input/FormInputs';
import ConditionalSection from './ConditionalSection';
import { useStore } from '../../../lib/providers/StoreProvider';
import SubSection from '../../wca/FormBuilder/SubSection';
import { useFormObject } from '../../wca/FormBuilder/provider/FormObjectProvider';

export default function Admin() {
const { admin } = useFormObject();
const { isAdminView, isPersisted } = useStore();
if (!isPersisted || !isAdminView) return null;

return (
<SubSection section="admin">
<InputBoolean id="isConfirmed" />
<InputBoolean id="isVisible" />
<InputBooleanSelect id="autoAcceptEnabled" required />
<ConditionalSection showIf={admin.autoAcceptEnabled}>
<InputNumber id="autoAcceptDisableThreshold" />
</ConditionalSection>
</SubSection>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Button, Icon } from 'semantic-ui-react';
import React, {
useMemo, useReducer, useRef,
useMemo, useReducer, useRef, useState,
} from 'react';
import {
Checkbox, Form, Header, Segment, Sticky,
Expand All @@ -14,6 +15,7 @@ import { useDispatch } from '../../../lib/providers/StoreProvider';
import I18n from '../../../lib/i18n';
import Loading from '../../Requests/Loading';
import { bulkUpdateRegistrations } from '../api/registration/patch/update_registration';
import { disableAutoAccept } from '../api/registration/patch/auto_accept';
import RegistrationAdministrationTable from './RegistrationsAdministrationTable';
import useCheckboxState from '../../../lib/hooks/useCheckboxState';
import { countries } from '../../../lib/wca-data.js.erb';
Expand Down Expand Up @@ -128,6 +130,8 @@ export default function RegistrationAdministrationList({ competitionInfo }) {

const actionsRef = useRef();

const [autoAcceptEnabled, setAutoAcceptEnabled] = useState(competitionInfo.auto_accept_registrations)
FinnIckler marked this conversation as resolved.
Show resolved Hide resolved

const [state, dispatchSort] = useReducer(sortReducer, {
sortColumn: competitionInfo['using_payment_integrations?']
? 'paid_on_with_registered_on_fallback'
Expand Down Expand Up @@ -160,6 +164,22 @@ export default function RegistrationAdministrationList({ competitionInfo }) {
},
});


const { mutate: disableAutoAcceptMutation, isPending: isUpdating } = useMutation({
mutationFn: disableAutoAccept,
onError: (data) => {
const { error } = data.json;
kr-matthews marked this conversation as resolved.
Show resolved Hide resolved
dispatchStore(setMessage(
`competitions.registration_v2.auto_accept.cant_disable`,
'negative',
));
},
onSuccess: () => {
dispatchStore(setMessage('competitions.registration_v2.auto_accept.disabled', 'positive'));
setAutoAcceptEnabled(false)
},
});

const { mutate: updateRegistrationMutation, isPending: isMutating } = useMutation({
mutationFn: bulkUpdateRegistrations,
onError: (data) => {
Expand Down Expand Up @@ -308,6 +328,21 @@ export default function RegistrationAdministrationList({ competitionInfo }) {
<Loading />
) : (
<Segment loading={isMutating} style={{ overflowX: 'scroll' }}>

{ autoAcceptEnabled && (
<>
<Button
disabled={isUpdating}
color="red"
onClick={() => disableAutoAcceptMutation(competitionInfo.id)}
>
<Icon name="ban" />
{' '}
{I18n.t('competitions.registration_v2.auto_accept.disable')}
</Button>
</>
)}
kr-matthews marked this conversation as resolved.
Show resolved Hide resolved

<Form>
<Form.Group widths="equal">
{Object.entries(expandableColumns).map(([id, name]) => (
Expand Down Expand Up @@ -344,9 +379,11 @@ export default function RegistrationAdministrationList({ competitionInfo }) {
{pending.length}
)
</Header>

<Header.Subheader>
{I18n.t('competitions.registration_v2.list.pending.information')}
</Header.Subheader>

<RegistrationAdministrationTable
columnsExpanded={expandedColumns}
registrations={pending}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { fetchJsonOrError } from '../../../../../lib/requests/fetchWithAuthenticityToken';
import { disableAutoAcceptUrl } from '../../../../../lib/requests/routes.js.erb';

export default async function disableAutoAccept(competitionId) {
const route = disableAutoAcceptUrl(competitionId);
const { data } = await fetchJsonOrError(route, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
});
return data;
}
2 changes: 2 additions & 0 deletions app/webpacker/lib/requests/routes.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ export const updateRegistrationUrl = `<%= CGI.unescape(Rails.application.routes.

export const bulkUpdateRegistrationUrl = `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v1_registrations_bulk_update_path) %>`;

export const disableAutoAcceptUrl = (competitionId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v0_competition_disable_auto_accept_path(competition_id: "${competitionId}")) %>`;

export const paymentTicketUrl = (competitionId, donationIso) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v1_registrations_payment_ticket_path(competition_id: "${competitionId}", donation_iso: "${donationIso}")) %>`;

export const serverStatusPageUrl = `<%= CGI.unescape(Rails.application.routes.url_helpers.server_status_path) %>`;
Loading
Loading