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

Commit

Permalink
Allow Importing of Registration (#314)
Browse files Browse the repository at this point in the history
* added frontend for importing

* add route

* don't check if guest status is restricted

* add import route to index

* added csv import

* correctly redirect to registrations edit

* run rubocop

* run eslint
  • Loading branch information
FinnIckler authored Nov 7, 2023
1 parent 7d7d76f commit b387c91
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 27 deletions.
3 changes: 2 additions & 1 deletion Frontend/.eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/src/api/schema.d.ts
/src/api/schema.d.ts
dist
20 changes: 20 additions & 0 deletions Frontend/src/api/registration/post/import_registration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getJWT } from '../../auth/get_jwt'

export default async function importRegistration(body: {
competitionId: string
file: File
}): Promise<{ status: string }> {
const formData = new FormData()
formData.append('csv_data', body.file)
const response = await fetch(
`${process.env.API_URL}/${body.competitionId}/import`,
{
method: 'POST',
body: formData,
headers: {
Authorization: await getJWT(),
},
}
)
return response.json()
}
5 changes: 5 additions & 0 deletions Frontend/src/index.dev.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom'
import { Container } from 'semantic-ui-react'
import Events from './pages/events'
import HomePage from './pages/home'
import Import from './pages/import'
import Register from './pages/register'
import RegistrationAdministration from './pages/registration_administration'
import RegistrationEdit from './pages/registration_edit'
Expand Down Expand Up @@ -81,6 +82,10 @@ const router = createBrowserRouter([
path: `${BASE_ROUTE}/:competition_id/events`,
element: <Events />,
},
{
path: `${BASE_ROUTE}/:competition_id/import`,
element: <Import />,
},
{
path: `${BASE_ROUTE}/:competition_id/schedule`,
element: <Schedule />,
Expand Down
42 changes: 42 additions & 0 deletions Frontend/src/pages/import/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useMutation } from '@tanstack/react-query'
import React, { useContext, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Button, Input, Segment } from 'semantic-ui-react'
import { CompetitionContext } from '../../api/helper/context/competition_context'
import { PermissionsContext } from '../../api/helper/context/permission_context'
import importRegistration from '../../api/registration/post/import_registration'
import { BASE_ROUTE } from '../../routes'
import PermissionMessage from '../../ui/messages/permissionMessage'

export default function Import() {
const [file, setFile] = useState()
const navigate = useNavigate()
const { competitionInfo } = useContext(CompetitionContext)
const { canAdminCompetition } = useContext(PermissionsContext)
const { mutate: importMutation, isLoading: isMutating } = useMutation({
mutationFn: importRegistration,
onSuccess: () =>
navigate(`${BASE_ROUTE}/${competitionInfo.id}/registrations/edit`),
})
return !canAdminCompetition ? (
<PermissionMessage>
You are not allowed to import registrations.
</PermissionMessage>
) : (
<Segment>
<Input
type="file"
accept="text/csv"
onChange={(event) => setFile(event.target.files[0])}
/>
<Button
disabled={!file || isMutating}
onClick={() =>
importMutation({ competitionId: competitionInfo.id, file })
}
>
Upload CSV
</Button>
</Segment>
)
}
38 changes: 18 additions & 20 deletions Frontend/src/pages/register/components/CompetingStep.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,26 +196,24 @@ export default function CompetingStep({ nextStep }) {
<div className={styles.eventSelectionText}>
<div className={styles.eventSelectionHeading}>Guests</div>
</div>
{competitionInfo.guest_entry_status !== 'restricted' && (
<div className={styles.commentWrapper}>
<Dropdown
value={guests}
onChange={(e, data) => setGuests(data.value)}
selection
options={[
...new Array(
(competitionInfo.guests_per_registration_limit ?? 99) + 1 // Arrays start at 0
),
].map((_, index) => {
return {
key: `registration-guest-dropdown-${index}`,
text: index,
value: index,
}
})}
/>
</div>
)}
<div className={styles.commentWrapper}>
<Dropdown
value={guests}
onChange={(e, data) => setGuests(data.value)}
selection
options={[
...new Array(
(competitionInfo.guests_per_registration_limit ?? 99) + 1 // Arrays start at 0
),
].map((_, index) => {
return {
key: `registration-guest-dropdown-${index}`,
text: index,
value: index,
}
})}
/>
</div>
</div>
<div className={styles.registrationRow}>
{registration?.competing?.registration_status ? (
Expand Down
5 changes: 5 additions & 0 deletions Frontend/src/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Outlet } from 'react-router-dom'
import { Container } from 'semantic-ui-react'
import Events from './pages/events'
import HomePage from './pages/home'
import Import from './pages/import'
import Register from './pages/register'
import RegistrationAdministration from './pages/registration_administration'
import RegistrationEdit from './pages/registration_edit'
Expand Down Expand Up @@ -55,6 +56,10 @@ const routes = [
path: `${BASE_ROUTE}/:competition_id/schedule`,
element: <Schedule />,
},
{
path: `${BASE_ROUTE}/:competition_id/import`,
element: <Import />,
},
{
path: `${BASE_ROUTE}/:competition_id/register`,
element: <Register />,
Expand Down
20 changes: 17 additions & 3 deletions app/controllers/registration_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def show
def validate_show_registration
@user_id, @competition_id = show_params
raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) unless
@current_user == @user_id || UserApi.can_administer?(@current_user, @competition_id)
@current_user.to_s == @user_id.to_s || UserApi.can_administer?(@current_user, @competition_id)
end

def payment_ticket
Expand Down Expand Up @@ -220,6 +220,20 @@ def list_admin
status: :internal_server_error
end

def import
file = params.require(:csv_data)
content = File.read(file)
if CsvImport.valid?(content)
registrations = CSV.parse(File.read(file), headers: true).map do |row|
CsvImport.parse_row_to_registration(row.to_h, params[:competition_id])
end
Registration.import(registrations)
render json: { status: 'Successfully imported registration' }
else
render json: { error: 'Invalid csv' }, status: :internal_server_error
end
end

private

def registration_params
Expand Down Expand Up @@ -333,14 +347,14 @@ def user_can_create_registration!
end

def organizer_signing_up_themselves?
@competition.is_organizer_or_delegate?(@current_user) && (@current_user == @user_id.to_s)
@competition.is_organizer_or_delegate?(@current_user) && (@current_user.to_s == @user_id.to_s)
end

def is_admin_or_current_user?
# Only an admin or the user themselves can create a registration for the user
# One case where admins need to create registrations for users is if a 3rd-party registration system is being used, and registration data is being
# passed to the Registration Service from it
(@current_user == @user_id.to_s) || UserApi.can_administer?(@current_user, @competition_id)
(@current_user.to_s == @user_id.to_s) || UserApi.can_administer?(@current_user, @competition_id)
end

def validate_status!
Expand Down
8 changes: 5 additions & 3 deletions app/helpers/lane_factory.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# frozen_string_literal: true

require 'time'

# rubocop:disable Metrics/ParameterLists
class LaneFactory
def self.competing_lane(event_ids = [], comment = '', guests = 0)
def self.competing_lane(event_ids = [], comment = '', guests = 0, admin_comment = '', registration_status = 'pending')
competing_lane = Lane.new({})
competing_lane.lane_name = 'competing'
competing_lane.completed_steps = ['Event Registration']
competing_lane.lane_state = 'pending'
competing_lane.lane_state = registration_status
competing_lane.lane_details = {
event_details: event_ids.map { |event_id| { event_id: event_id } },
comment: comment,
admin_comment: admin_comment,
guests: guests,
}
competing_lane
Expand All @@ -31,3 +32,4 @@ def self.payment_lane(fee_lowest_denominator, currency_code, payment_id)
payment_lane
end
end
# rubocop:enable Metrics/ParameterLists
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
get '/api/v1/registrations/:competition_id/admin', to: 'registration#list_admin'
get '/api/v1/registrations/:competition_id', to: 'registration#list'
get '/api/v1/:competition_id/payment', to: 'registration#payment_ticket'
post '/api/v1/:competition_id/import', to: 'registration#import'
end
2 changes: 2 additions & 0 deletions csv_template.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
user_id,guests,competing.event_ids,competing.registration_status,competing.registered_on,competing.comment,competing.admin_comment
15073,0,333;444,pending,2023-11-03T15:47:24.764Z,test,test2
20 changes: 20 additions & 0 deletions lib/csv_import.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module CsvImport
HEADERS = %w[user_id guests competing.event_ids competing.registration_status competing.registered_on competing.comment competing.admin_comment].freeze
def self.valid?(csv)
data = CSV.parse(csv)
headers = data.first
headers.present? && headers.all? { |h| h.in?(HEADERS) }
end

def self.parse_row_to_registration(csv_hash, competition_id)
{
attendee_id: "#{competition_id}-#{csv_hash["user_id"]}",
user_id: csv_hash['user_id'],
competition_id: competition_id,
lanes: [LaneFactory.competing_lane(csv_hash['competing.event_ids'].split(';'), csv_hash['competing.comment'], csv_hash['guests'], csv_hash['competing.admin_comment'], csv_hash['competing.registration_status'])],
isCompeting: csv_hash['competing.registration_status'] == 'accepted',
}
end
end

0 comments on commit b387c91

Please sign in to comment.