diff --git a/Frontend/.eslintignore b/Frontend/.eslintignore
index 72567dcd..c960e3ba 100644
--- a/Frontend/.eslintignore
+++ b/Frontend/.eslintignore
@@ -1 +1,2 @@
-/src/api/schema.d.ts
\ No newline at end of file
+/src/api/schema.d.ts
+dist
\ No newline at end of file
diff --git a/Frontend/src/api/registration/post/import_registration.ts b/Frontend/src/api/registration/post/import_registration.ts
new file mode 100644
index 00000000..098b2a33
--- /dev/null
+++ b/Frontend/src/api/registration/post/import_registration.ts
@@ -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()
+}
diff --git a/Frontend/src/index.dev.jsx b/Frontend/src/index.dev.jsx
index 7b3175a5..b0422d9e 100644
--- a/Frontend/src/index.dev.jsx
+++ b/Frontend/src/index.dev.jsx
@@ -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'
@@ -81,6 +82,10 @@ const router = createBrowserRouter([
path: `${BASE_ROUTE}/:competition_id/events`,
element: ,
},
+ {
+ path: `${BASE_ROUTE}/:competition_id/import`,
+ element: ,
+ },
{
path: `${BASE_ROUTE}/:competition_id/schedule`,
element: ,
diff --git a/Frontend/src/pages/import/index.jsx b/Frontend/src/pages/import/index.jsx
new file mode 100644
index 00000000..eb8f4a3a
--- /dev/null
+++ b/Frontend/src/pages/import/index.jsx
@@ -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 ? (
+
+ You are not allowed to import registrations.
+
+ ) : (
+
+ setFile(event.target.files[0])}
+ />
+
+
+ )
+}
diff --git a/Frontend/src/pages/register/components/CompetingStep.jsx b/Frontend/src/pages/register/components/CompetingStep.jsx
index 1e1a4102..32a59cfa 100644
--- a/Frontend/src/pages/register/components/CompetingStep.jsx
+++ b/Frontend/src/pages/register/components/CompetingStep.jsx
@@ -196,26 +196,24 @@ export default function CompetingStep({ nextStep }) {
- {competitionInfo.guest_entry_status !== 'restricted' && (
-
- 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,
- }
- })}
- />
-
- )}
+
+ 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,
+ }
+ })}
+ />
+
{registration?.competing?.registration_status ? (
diff --git a/Frontend/src/routes.jsx b/Frontend/src/routes.jsx
index 59ff92f5..65f52567 100644
--- a/Frontend/src/routes.jsx
+++ b/Frontend/src/routes.jsx
@@ -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'
@@ -55,6 +56,10 @@ const routes = [
path: `${BASE_ROUTE}/:competition_id/schedule`,
element: ,
},
+ {
+ path: `${BASE_ROUTE}/:competition_id/import`,
+ element: ,
+ },
{
path: `${BASE_ROUTE}/:competition_id/register`,
element: ,
diff --git a/app/controllers/registration_controller.rb b/app/controllers/registration_controller.rb
index 57fff8cd..4cd32af3 100644
--- a/app/controllers/registration_controller.rb
+++ b/app/controllers/registration_controller.rb
@@ -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
@@ -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
@@ -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!
diff --git a/app/helpers/lane_factory.rb b/app/helpers/lane_factory.rb
index 88f1e062..fedc8ad7 100644
--- a/app/helpers/lane_factory.rb
+++ b/app/helpers/lane_factory.rb
@@ -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
@@ -31,3 +32,4 @@ def self.payment_lane(fee_lowest_denominator, currency_code, payment_id)
payment_lane
end
end
+# rubocop:enable Metrics/ParameterLists
diff --git a/config/routes.rb b/config/routes.rb
index 55f14e70..f101a9a8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/csv_template.csv b/csv_template.csv
new file mode 100644
index 00000000..40c56256
--- /dev/null
+++ b/csv_template.csv
@@ -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
\ No newline at end of file
diff --git a/lib/csv_import.rb b/lib/csv_import.rb
new file mode 100644
index 00000000..57cb7461
--- /dev/null
+++ b/lib/csv_import.rb
@@ -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