Skip to content
This repository has been archived by the owner on Jan 3, 2025. It is now read-only.

Commit

Permalink
Add Public Waiting List Tab (#295)
Browse files Browse the repository at this point in the history
* Waiting List Frontend

* implement waiting list management

* run rubocop

* fix eslint

* correctly show the number of waiting competitors

* check length of waiting too

* don't force organizers to update the waitlist everytime someone is approved

* adding tests for waiting list

* waiting list tests passing

* refactored waiting list functions

* added cache invalidations

* corrected typing of test values and updated type acceptance tests

* added tests for waiting list position outside of min/max boundary

* re-enabled cache

* caching tests

* Fixed caches not updating and introduced a method in lib

* corrected cache behaviour

* removed puts statements

* removed get/set and refactored to use minmax

* removed caching-test config files

* remove cache_test fro mgemfile

* refactored list_waiting to use get_by_status

* skipping jwt validation on list_waiting temproarily

* removed caching of waiting list

* removed commented code and unnecessary docs

* fix issue with worker not loading lane

* fix issue with multiple updates not waiting before the other completes

* fix typo

* Add Segment for Waiting List

* run eslint

* added redis to backend-test compose file

---------

Co-authored-by: Duncan <duncanonthejob@gmail.com>
  • Loading branch information
FinnIckler and dunkOnIT authored Dec 15, 2023
1 parent f3110e7 commit 072b266
Show file tree
Hide file tree
Showing 24 changed files with 813 additions and 167 deletions.
17 changes: 17 additions & 0 deletions Frontend/src/api/registration/get/get_registrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export async function getAllRegistrations(
}
throw new BackendError(error.error, response.status)
}

return addUserInfo(data!)
}

Expand All @@ -77,3 +78,19 @@ export async function getSingleRegistration(
{ needsAuthentication: true }
) as Promise<{ registration: components['schemas']['registrationAdmin'] }>
}

export async function getWaitingCompetitors(
competitionId: string
): Promise<components['schemas']['registrationAdmin'][]> {
const registrations = (await backendFetch(
`/registrations/${competitionId}/waiting`,
'GET',
{
needsAuthentication: false,
}
)) as components['schemas']['registrationAdmin'][]

return (await addUserInfo(
registrations
)) as components['schemas']['registrationAdmin'][]
}
5 changes: 5 additions & 0 deletions Frontend/src/index.dev.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Registrations from './pages/registrations'
import Schedule from './pages/schedule'
import TestLogin from './pages/test/login'
import TestLogout from './pages/test/logout'
import Waiting from './pages/waiting'
import { BASE_ROUTE } from './routes'
import App from './ui/App'
import Competition from './ui/Competition'
Expand Down Expand Up @@ -103,6 +104,10 @@ const router = createBrowserRouter([
path: 'register',
element: <Register />,
},
{
path: `waiting`,
element: <Waiting />,
},
{
path: 'tabs/:tab_id',
element: <CustomTab />,
Expand Down
2 changes: 1 addition & 1 deletion Frontend/src/pages/register/components/CompetingStep.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ export default function CompetingStep({ nextStep }) {
<>
<Message info icon floating>
<Popup
content="You will only be accepted if you have met all reigstration requirements"
content="You will only be accepted if you have met all registration requirements"
position="top left"
trigger={<Icon name="circle info" />}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ export default function RegistrationActions({
}
}

const changeStatus = async (attendees, status) => {
attendees.forEach((attendee) => {
updateRegistrationMutation(
const changeStatus = (attendees, status) => {
attendees.forEach(async (attendee) => {
await updateRegistrationMutation(
{
user_id: attendee,
competing: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Button,
Checkbox,
Header,
Input,
Message,
Segment,
TextArea,
Expand All @@ -28,6 +29,8 @@ export default function RegistrationEditor() {
const [comment, setComment] = useState('')
const [adminComment, setAdminComment] = useState('')
const [status, setStatus] = useState('')
const [waitingListPosition, setWaitingListPosition] = useState(0)
const [guests, setGuests] = useState(0)
const [selectedEvents, setSelectedEvents] = useState([])
const [registration, setRegistration] = useState({})
const [isCheckingRefunds, setIsCheckingRefunds] = useState(false)
Expand Down Expand Up @@ -73,6 +76,10 @@ export default function RegistrationEditor() {
setAdminComment(
serverRegistration.registration.competing.admin_comment ?? ''
)
setWaitingListPosition(
serverRegistration.registration.competing.waiting_list_position ?? 0
)
setGuests(serverRegistration.registration.guests ?? 0)
}
}, [serverRegistration])

Expand Down Expand Up @@ -127,22 +134,24 @@ export default function RegistrationEditor() {
event_ids: selectedEvents,
comment,
admin_comment: adminComment,
waiting_list_position: waitingListPosition,
},
competition_id: competitionInfo.id,
})
}
}, [
adminComment,
comment,
hasChanges,
commentIsValid,
competitionInfo.id,
eventsAreValid,
hasChanges,
selectedEvents,
status,
maxEvents,
updateRegistrationMutation,
user_id,
maxEvents,
status,
selectedEvents,
comment,
adminComment,
waitingListPosition,
competitionInfo.id,
])

const registrationEditDeadlinePassed = moment(
Expand Down Expand Up @@ -228,6 +237,16 @@ export default function RegistrationEditor() {
checked={status === 'cancelled'}
onChange={(_, data) => setStatus(data.value)}
/>
<br />
<Header>Guests</Header>
<Input
disabled={registrationEditDeadlinePassed}
type="number"
min={0}
max={99}
value={guests}
onChange={(_, data) => setGuests(data.value)}
/>
</div>

{registrationEditDeadlinePassed ? (
Expand Down
53 changes: 53 additions & 0 deletions Frontend/src/pages/waiting/components/WaitingList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useQuery } from '@tanstack/react-query'
import React, { useContext } from 'react'
import { Table, TableFooter } from 'semantic-ui-react'
import { CompetitionContext } from '../../../api/helper/context/competition_context'
import { getWaitingCompetitors } from '../../../api/registration/get/get_registrations'
import { setMessage } from '../../../ui/events/messages'
import LoadingMessage from '../../../ui/messages/loadingMessage'

export default function WaitingList() {
const { competitionInfo } = useContext(CompetitionContext)
const { isLoading, data: waiting } = useQuery({
queryKey: ['waiting', competitionInfo.id],
queryFn: () => getWaitingCompetitors(competitionInfo.id),
retry: false,
onError: (err) => {
setMessage(err.message, 'error')
},
})
return isLoading ? (
<LoadingMessage />
) : (
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Position</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{waiting?.length ? (
waiting
.sort(
(w1, w2) =>
w1.competing.waiting_list_position -
w2.competing.waiting_list_position
) // Once a waiting list is established, we just care about the order of the waitlisted competitors
.map((w, i) => (
<Table.Row key={w.user_id}>
<Table.Cell>{w.user.name}</Table.Cell>
<Table.Cell>
{w.competing.waiting_list_position === 0
? 'Not yet assigned'
: i + 1}
</Table.Cell>
</Table.Row>
))
) : (
<TableFooter>No one on the Waiting List.</TableFooter>
)}
</Table.Body>
</Table>
)
}
12 changes: 12 additions & 0 deletions Frontend/src/pages/waiting/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'
import { Header, Segment } from 'semantic-ui-react'
import WaitingList from './components/WaitingList'

export default function Waiting() {
return (
<Segment padded attached>
<Header>Waiting List:</Header>
<WaitingList />
</Segment>
)
}
5 changes: 5 additions & 0 deletions Frontend/src/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import RegistrationAdministration from './pages/registration_administration'
import RegistrationEdit from './pages/registration_edit'
import Registrations from './pages/registrations'
import Schedule from './pages/schedule'
import Waiting from './pages/waiting'
import App from './ui/App'
import Competition from './ui/Competition'
import CustomTab from './ui/CustomTab'
Expand Down Expand Up @@ -72,6 +73,10 @@ const routes = [
path: 'tabs/:tab_id',
element: <CustomTab />,
},
{
path: `waiting`,
element: <Waiting />,
},
{
path: 'registrations',
element: <Registrations />,
Expand Down
7 changes: 7 additions & 0 deletions Frontend/src/ui/PageTabs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function PageTabs() {
}
if (canAdminCompetition) {
optionalTabs.push(registrationsMenuConfig)
optionalTabs.push(waitingMenuConfig)
}
if (new Date(competitionInfo.registration_open) < Date.now()) {
optionalTabs.push(competitorsMenuConfig)
Expand Down Expand Up @@ -143,6 +144,12 @@ const registrationsMenuConfig = {
icon: 'list ul',
label: 'Registrations',
}
const waitingMenuConfig = {
key: 'waiting',
route: 'waiting',
icon: 'clock',
label: 'Waiting list',
}
const competitorsMenuConfig = {
key: 'competitors',
route: 'registrations',
Expand Down
51 changes: 44 additions & 7 deletions app/controllers/registration_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
require_relative '../helpers/error_codes'

class RegistrationController < ApplicationController
skip_before_action :validate_token, only: [:list]
skip_before_action :validate_token, only: [:list, :list_waiting]
# The order of the validations is important to not leak any non public info via the API
# That's why we should always validate a request first, before taking any other before action
# before_actions are triggered in the order they are defined
Expand All @@ -32,7 +32,7 @@ def create
lane_name: 'competing',
step: 'Event Registration',
step_details: {
registration_status: 'waiting',
registration_status: 'pending',
event_ids: event_ids,
comment: comment,
guests: guests,
Expand Down Expand Up @@ -90,28 +90,31 @@ def update
comment = params.dig('competing', 'comment')
event_ids = params.dig('competing', 'event_ids')
admin_comment = params.dig('competing', 'admin_comment')
waiting_list_position = params.dig('competing', 'waiting_list_position')

begin
registration = Registration.find("#{@competition_id}-#{@user_id}")
old_status = registration.competing_status
updated_registration = registration.update_competing_lane!({ status: status, comment: comment, event_ids: event_ids, admin_comment: admin_comment, guests: guests })
if old_status == 'accepted' && status != 'accepted'
Registration.decrement_competitors_count(@competition_id)
elsif old_status != 'accepted' && status == 'accepted'
Registration.increment_competitors_count(@competition_id)
end
updated_registration = registration.update_competing_lane!({ status: status, comment: comment, event_ids: event_ids, admin_comment: admin_comment, guests: guests, waiting_list_position: waiting_list_position })

render json: { status: 'ok', registration: {
user_id: updated_registration['user_id'],
guests: updated_registration.guests,
guests: updated_registration['guests'],
competing: {
event_ids: updated_registration.registered_event_ids,
registration_status: updated_registration.competing_status,
registered_on: updated_registration['created_at'],
comment: updated_registration.competing_comment,
admin_comment: updated_registration.admin_comment,
waiting_list_position: updated_registration.competing_waiting_list_position,
},
} }
rescue StandardError => e
rescue Dynamoid::Errors::Error => e
puts e
Metrics.registration_dynamodb_errors_counter.increment
render json: { error: "Error Updating Registration: #{e.message}" },
Expand Down Expand Up @@ -170,7 +173,39 @@ def list
competition_id = list_params
registrations = get_registrations(competition_id, only_attending: true)
render json: registrations
rescue StandardError => e
rescue Dynamoid::Errors::Error => e
# Render an error response
puts e
Metrics.registration_dynamodb_errors_counter.increment
render json: { error: "Error getting registrations #{e}" },
status: :internal_server_error
end

def mine
my_registrations = Registration.where(user_id: @current_user).map { |x| { competition_id: x.competition_id, status: x.competing_status } }
render json: { registrations: my_registrations }
rescue Dynamoid::Errors::Error => e
# Render an error response
puts e
Metrics.registration_dynamodb_errors_counter.increment
render json: { error: "Error getting registrations #{e}" },
status: :internal_server_error
end

def list_waiting
competition_id = list_params

waiting = Registration.get_registrations_by_status(competition_id, 'waiting_list').map do |registration|
{
user_id: registration[:user_id],
competing: {
event_ids: registration.event_ids,
waiting_list_position: registration.competing_waiting_list_position || 0,
},
}
end
render json: waiting
rescue Dynamoid::Errors::Error => e
# Render an error response
puts e
Metrics.registration_dynamodb_errors_counter.increment
Expand All @@ -190,7 +225,7 @@ def validate_list_admin
def list_admin
registrations = get_registrations(@competition_id)
render json: registrations
rescue StandardError => e
rescue Dynamoid::Errors::Error => e
puts e
# Is there a reason we aren't using an error code here?
Metrics.registration_dynamodb_errors_counter.increment
Expand Down Expand Up @@ -254,6 +289,7 @@ def get_registrations(competition_id, only_attending: false)
registered_on: x['created_at'],
comment: x.competing_comment,
admin_comment: x.admin_comment,
waiting_list_position: x.competing_waiting_list_position,
},
payment: {
payment_status: x.payment_status,
Expand All @@ -275,6 +311,7 @@ def get_single_registration(user_id, competition_id)
registered_on: registration['created_at'],
comment: registration.competing_comment,
admin_comment: registration.admin_comment,
waiting_list_position: registration.competing_waiting_list_position,
},
payment: {
payment_status: registration.payment_status,
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/error_codes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ module ErrorCodes
INVALID_REGISTRATION_STATUS = -4007
REGISTRATION_CLOSED = -4008
ORGANIZER_MUST_CANCEL_REGISTRATION = -4009
INVALID_WAITING_LIST_POSITION = -4010
MUST_ACCEPT_WAITING_LIST_LEADER = -4011

# Payment Errors
PAYMENT_NOT_ENABLED = -3001
Expand Down
4 changes: 3 additions & 1 deletion app/helpers/lane_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

require 'time'
class LaneFactory
def self.competing_lane(event_ids: [], comment: '', admin_comment: '', registration_status: 'pending')
# TODO: try again to set waiting_list_position form the tests, instead of directly in the lane factory
def self.competing_lane(event_ids: [], comment: '', admin_comment: '', registration_status: 'pending', waiting_list_position: nil)
competing_lane = Lane.new({})
competing_lane.lane_name = 'competing'
competing_lane.completed_steps = ['Event Registration']
Expand All @@ -11,6 +12,7 @@ def self.competing_lane(event_ids: [], comment: '', admin_comment: '', registrat
'event_details' => event_ids.map { |event_id| { event_id: event_id, event_registration_state: registration_status } },
'comment' => comment,
'admin_comment' => admin_comment,
'waiting_list_position' => waiting_list_position.to_i,
}
competing_lane
end
Expand Down
Loading

0 comments on commit 072b266

Please sign in to comment.