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 }) {
Guests
- {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