diff --git a/README.md b/README.md index 58d5bd50b..7a30a9509 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![GitHub last commit](https://img.shields.io/github/last-commit/softwaremagico/KendoTournamentManager)](https://github.com/softwaremagico/KendoTournamentManager) [![Issues](https://img.shields.io/github/issues/softwaremagico/KendoTournamentManager.svg)](https://github.com/softwaremagico/KendoTournamentManager/issues) [![CircleCI](https://circleci.com/gh/softwaremagico/KendoTournamentManager.svg?style=shield)](https://circleci.com/gh/softwaremagico/KendoTournamentManager) -[![Time](https://img.shields.io/badge/development-717.5h-blueviolet.svg)]() +[![Time](https://img.shields.io/badge/development-724.5h-blueviolet.svg)]() [![Powered by](https://img.shields.io/badge/powered%20by%20java-orange.svg?logo=OpenJDK&logoColor=white)]() [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=kendo-tournament-backend&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=kendo-tournament-backend) diff --git a/backend/kendo-tournament-core/pom.xml b/backend/kendo-tournament-core/pom.xml index 851a9be18..89c8bf88b 100644 --- a/backend/kendo-tournament-core/pom.xml +++ b/backend/kendo-tournament-core/pom.xml @@ -9,7 +9,7 @@ kendo-tournament-backend com.softwaremagico - 2.18.1 + 2.19.0 diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/controller/CsvController.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/controller/CsvController.java new file mode 100644 index 000000000..3901d8fb8 --- /dev/null +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/controller/CsvController.java @@ -0,0 +1,186 @@ +package com.softwaremagico.kt.core.controller; + +/*- + * #%L + * Kendo Tournament Manager (Core) + * %% + * Copyright (C) 2021 - 2025 SoftwareMagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.softwaremagico.kt.core.controller.models.ClubDTO; +import com.softwaremagico.kt.core.controller.models.ParticipantDTO; +import com.softwaremagico.kt.core.controller.models.TeamDTO; +import com.softwaremagico.kt.core.converters.ClubConverter; +import com.softwaremagico.kt.core.converters.ParticipantConverter; +import com.softwaremagico.kt.core.converters.TeamConverter; +import com.softwaremagico.kt.core.converters.models.ClubConverterRequest; +import com.softwaremagico.kt.core.converters.models.ParticipantConverterRequest; +import com.softwaremagico.kt.core.converters.models.TeamConverterRequest; +import com.softwaremagico.kt.core.csv.ClubCsv; +import com.softwaremagico.kt.core.csv.ParticipantCsv; +import com.softwaremagico.kt.core.csv.TeamCsv; +import com.softwaremagico.kt.core.exceptions.InvalidCsvFieldException; +import com.softwaremagico.kt.core.providers.ClubProvider; +import com.softwaremagico.kt.core.providers.ParticipantProvider; +import com.softwaremagico.kt.core.providers.RoleProvider; +import com.softwaremagico.kt.core.providers.TeamProvider; +import com.softwaremagico.kt.logger.KendoTournamentLogger; +import com.softwaremagico.kt.persistence.entities.Club; +import com.softwaremagico.kt.persistence.entities.Participant; +import com.softwaremagico.kt.persistence.entities.Role; +import com.softwaremagico.kt.persistence.entities.Team; +import com.softwaremagico.kt.persistence.values.RoleType; +import org.springframework.stereotype.Controller; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Controller +public class CsvController { + + private final ClubCsv clubCsv; + private final ClubProvider clubProvider; + private final ClubConverter clubConverter; + + private final ParticipantCsv participantCsv; + private final ParticipantProvider participantProvider; + private final ParticipantConverter participantConverter; + + private final TeamCsv teamCsv; + private final TeamProvider teamProvider; + private final TeamConverter teamConverter; + + private final RoleProvider roleProvider; + + public CsvController(ClubCsv clubCsv, ClubProvider clubProvider, ClubConverter clubConverter, + ParticipantCsv participantCsv, ParticipantProvider participantProvider, ParticipantConverter participantConverter, + TeamCsv teamCsv, TeamProvider teamProvider, TeamConverter teamConverter, RoleProvider roleProvider) { + this.clubCsv = clubCsv; + this.clubProvider = clubProvider; + this.clubConverter = clubConverter; + this.participantCsv = participantCsv; + this.participantProvider = participantProvider; + this.participantConverter = participantConverter; + this.teamCsv = teamCsv; + this.teamProvider = teamProvider; + this.teamConverter = teamConverter; + this.roleProvider = roleProvider; + } + + + public List addClubs(String csvContent, String uploadedBy) { + final List clubs = clubCsv.readCSV(csvContent); + final List failedClubs = new ArrayList<>(); + for (Club club : clubs) { + try { + if (club.getName() != null && club.getCity() != null) { + final Optional storedClub = clubProvider.findBy(club.getName(), club.getCity()); + if (storedClub.isPresent()) { + KendoTournamentLogger.warning(this.getClass(), "Club '" + club.getName() + "' from '" + + club.getCity() + "' already exists. Will be updated."); + club.setId(storedClub.get().getId()); + club.setUpdatedBy(uploadedBy); + } else { + club.setCreatedBy(uploadedBy); + } + clubProvider.save(club); + } else { + KendoTournamentLogger.warning(this.getClass(), "Club with invalid name and/or city."); + failedClubs.add(clubConverter.convert(new ClubConverterRequest(club))); + } + } catch (Exception e) { + KendoTournamentLogger.errorMessage(this.getClass(), e); + failedClubs.add(clubConverter.convert(new ClubConverterRequest(club))); + } + } + return failedClubs; + } + + + public List addParticipants(String csvContent, String uploadedBy) { + final List participants = participantCsv.readCSV(csvContent); + final List failedParticipants = new ArrayList<>(); + for (Participant participant : participants) { + try { + if (participantProvider.findByIdCard(participant.getIdCard()).isPresent()) { + KendoTournamentLogger.severe(this.getClass().getName(), "Participant '" + participant.getIdCard() + "' with name '" + + participant.getName() + " " + participant.getLastname() + "' already exists."); + participant.setUpdatedBy(uploadedBy); + failedParticipants.add(participantConverter.convert(new ParticipantConverterRequest(participant))); + } else { + participant.setCreatedBy(uploadedBy); + participantProvider.save(participant); + } + } catch (Exception e) { + KendoTournamentLogger.severe(this.getClass().getName(), "Error when inserting '" + participant + "'."); + KendoTournamentLogger.errorMessage(this.getClass(), e); + failedParticipants.add(participantConverter.convert(new ParticipantConverterRequest(participant))); + } + } + return failedParticipants; + } + + + public List addTeams(String csvContent, String uploadedBy) { + final List teams = teamCsv.readCSV(csvContent); + final List failedTeams = new ArrayList<>(); + for (Team team : teams) { + if (team.getTournament() == null) { + KendoTournamentLogger.severe(this.getClass().getName(), "Team '" + team.getName() + "' has assigned a tournament that does not exists."); + failedTeams.add(teamConverter.convert(new TeamConverterRequest(team))); + continue; + } + if (team.getMembers().size() > team.getTournament().getTeamSize()) { + throw new InvalidCsvFieldException(this.getClass(), "Team size is incorrect!", null); + } + setTeamMemberRoles(team); + try { + final Optional storedTeam = teamProvider.get(team.getTournament(), team.getName()); + if (storedTeam.isPresent()) { + KendoTournamentLogger.warning(this.getClass(), "Team '" + team.getName() + "' already exists on tournament '" + + team.getTournament().getName() + "'. Will be updated."); + team.setId(storedTeam.get().getId()); + team.setUpdatedBy(uploadedBy); + } else { + team.setCreatedBy(uploadedBy); + } + teamProvider.save(team); + } catch (Exception e) { + KendoTournamentLogger.errorMessage(this.getClass(), e); + failedTeams.add(teamConverter.convert(new TeamConverterRequest(team))); + } + } + return failedTeams; + } + + private void setTeamMemberRoles(Team team) { + if (team.getTournament() == null) { + return; + } + //Define roles for team members. + team.getMembers().forEach(member -> { + final Role role = roleProvider.get(team.getTournament(), member); + if (role == null) { + roleProvider.save(new Role(team.getTournament(), member, RoleType.COMPETITOR)); + } else { + role.setRoleType(RoleType.COMPETITOR); + roleProvider.save(role); + } + }); + } +} diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/ClubCsv.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/ClubCsv.java new file mode 100644 index 000000000..c0a98c307 --- /dev/null +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/ClubCsv.java @@ -0,0 +1,70 @@ +package com.softwaremagico.kt.core.csv; + +/*- + * #%L + * Kendo Tournament Manager (Core) + * %% + * Copyright (C) 2021 - 2025 SoftwareMagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.softwaremagico.kt.persistence.entities.Club; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class ClubCsv extends CsvReader { + private static final String NAME_HEADER = "name"; + private static final String COUNTRY_HEADER = "country"; + private static final String CITY_HEADER = "city"; + private static final String ADDRESS_HEADER = "address"; + private static final String EMAIL_HEADER = "email"; + private static final String PHONE_HEADER = "phone"; + private static final String WEB_HEADER = "web"; + + @Override + public List readCSV(String csvContent) { + final String[] headers = getHeaders(csvContent); + checkHeaders(headers, NAME_HEADER, COUNTRY_HEADER, CITY_HEADER, ADDRESS_HEADER, EMAIL_HEADER, PHONE_HEADER, WEB_HEADER); + final String[] content = getContent(csvContent); + final List clubs = new ArrayList<>(); + + final int nameIndex = getHeaderIndex(headers, NAME_HEADER); + final int countryIndex = getHeaderIndex(headers, COUNTRY_HEADER); + final int cityIndex = getHeaderIndex(headers, CITY_HEADER); + final int addressIndex = getHeaderIndex(headers, ADDRESS_HEADER); + final int emailIndex = getHeaderIndex(headers, EMAIL_HEADER); + final int phoneIndex = getHeaderIndex(headers, PHONE_HEADER); + final int webIndex = getHeaderIndex(headers, WEB_HEADER); + + + for (String clubLine : content) { + final Club club = new Club(); + club.setName(getField(clubLine, nameIndex)); + club.setCountry(getField(clubLine, countryIndex)); + club.setCity(getField(clubLine, cityIndex)); + club.setAddress(getField(clubLine, addressIndex)); + club.setEmail(getField(clubLine, emailIndex)); + club.setPhone(getField(clubLine, phoneIndex)); + club.setWeb(getField(clubLine, webIndex)); + clubs.add(club); + } + return clubs; + } + +} diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/CsvReader.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/CsvReader.java new file mode 100644 index 000000000..93d10d579 --- /dev/null +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/CsvReader.java @@ -0,0 +1,80 @@ +package com.softwaremagico.kt.core.csv; + +/*- + * #%L + * Kendo Tournament Manager (Core) + * %% + * Copyright (C) 2021 - 2025 SoftwareMagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.softwaremagico.kt.core.exceptions.InvalidCsvFieldException; +import com.softwaremagico.kt.persistence.entities.Element; + +import java.util.Arrays; +import java.util.List; + +public abstract class CsvReader { + private static final String CSV_SEPARATOR = ";"; + private static final String LINE_SEPARATOR = "\\r?\\n"; + + public abstract List readCSV(String csvContent); + + protected void checkHeaders(String[] fileHeaders, String... elementHeaders) throws InvalidCsvFieldException { + for (String header : fileHeaders) { + if (getHeaderIndex(elementHeaders, header) < 0) { + throw new InvalidCsvFieldException(this.getClass(), "Invalid header '" + header + "'.", header); + } + } + } + + public String[] getHeaders(String content) { + final String[] lines = content.replace("#", "").split(LINE_SEPARATOR); + if (lines.length > 1) { + return lines[0].split(CSV_SEPARATOR); + } + return new String[0]; + } + + + public String[] getContent(String content) { + final String[] lines = content.split("\\r?\\n"); + if (lines.length > 1) { + return Arrays.copyOfRange(lines, 1, lines.length); + } + return new String[0]; + } + + public int getHeaderIndex(String[] headers, String header) { + for (int i = 0; i < headers.length; i++) { + if (header != null && headers[i] != null && headers[i].trim().equalsIgnoreCase(header.trim())) { + return i; + } + } + return -1; + } + + public String getField(String line, int index) { + if (index < 0) { + return null; + } + final String[] columns = line.split(CSV_SEPARATOR); + if (index < columns.length) { + return columns[index].trim(); + } + return null; + } +} diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/ParticipantCsv.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/ParticipantCsv.java new file mode 100644 index 000000000..580761940 --- /dev/null +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/ParticipantCsv.java @@ -0,0 +1,80 @@ +package com.softwaremagico.kt.core.csv; + +/*- + * #%L + * Kendo Tournament Manager (Core) + * %% + * Copyright (C) 2021 - 2025 SoftwareMagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.softwaremagico.kt.core.providers.ClubProvider; +import com.softwaremagico.kt.logger.KendoTournamentLogger; +import com.softwaremagico.kt.persistence.entities.Participant; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class ParticipantCsv extends CsvReader { + private static final String NAME_HEADER = "name"; + private static final String LASTNAME_HEADER = "lastname"; + private static final String ID_CARD_HEADER = "idCard"; + private static final String CLUB_HEADER = "club"; + private static final String CLUB_CITY_HEADER = "clubCity"; + + private final ClubProvider clubProvider; + + public ParticipantCsv(ClubProvider clubProvider) { + this.clubProvider = clubProvider; + } + + @Override + public List readCSV(String csvContent) { + final String[] headers = getHeaders(csvContent); + checkHeaders(headers, NAME_HEADER, LASTNAME_HEADER, ID_CARD_HEADER, CLUB_HEADER, CLUB_CITY_HEADER); + final String[] content = getContent(csvContent); + final List participants = new ArrayList<>(); + + final int nameIndex = getHeaderIndex(headers, NAME_HEADER); + final int lastnameIndex = getHeaderIndex(headers, LASTNAME_HEADER); + final int idCardIndex = getHeaderIndex(headers, ID_CARD_HEADER); + final int clubIndex = getHeaderIndex(headers, CLUB_HEADER); + final int clubCityIndex = getHeaderIndex(headers, CLUB_CITY_HEADER); + + for (String participantLine : content) { + final Participant participant = new Participant(); + participant.setName(getField(participantLine, nameIndex)); + participant.setLastname(getField(participantLine, lastnameIndex)); + participant.setIdCard(getField(participantLine, idCardIndex)); + + final String clubName = getField(participantLine, clubIndex); + final String clubCity = getField(participantLine, clubCityIndex); + if (clubName != null && clubCity != null) { + try { + participant.setClub(clubProvider.findBy(clubName, clubCity).orElse(null)); + } catch (Exception e) { + KendoTournamentLogger.severe(this.getClass().getName(), "Error when inserting CSV from '" + participantLine + "'."); + KendoTournamentLogger.errorMessage(this.getClass(), e); + } + } + + participants.add(participant); + } + return participants; + } +} diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/TeamCsv.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/TeamCsv.java new file mode 100644 index 000000000..3325268d0 --- /dev/null +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/csv/TeamCsv.java @@ -0,0 +1,128 @@ +package com.softwaremagico.kt.core.csv; + +/*- + * #%L + * Kendo Tournament Manager (Core) + * %% + * Copyright (C) 2021 - 2025 SoftwareMagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.softwaremagico.kt.core.exceptions.TournamentNotFoundException; +import com.softwaremagico.kt.core.providers.ParticipantProvider; +import com.softwaremagico.kt.core.providers.TournamentProvider; +import com.softwaremagico.kt.logger.KendoTournamentLogger; +import com.softwaremagico.kt.persistence.entities.Participant; +import com.softwaremagico.kt.persistence.entities.Team; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class TeamCsv extends CsvReader { + private static final String NAME_HEADER = "name"; + private static final String TOURNAMENT_HEADER = "tournament"; + private static final String MEMBER1_HEADER = "member1"; + private static final String MEMBER2_HEADER = "member2"; + private static final String MEMBER3_HEADER = "member3"; + private static final String MEMBER4_HEADER = "member4"; + private static final String MEMBER5_HEADER = "member5"; + private static final String MEMBER6_HEADER = "member6"; + private static final String MEMBER7_HEADER = "member7"; + private static final String MEMBER8_HEADER = "member8"; + private static final String MEMBER9_HEADER = "member9"; + + private final TournamentProvider tournamentProvider; + private final ParticipantProvider participantProvider; + + public TeamCsv(TournamentProvider tournamentProvider, ParticipantProvider participantProvider) { + this.tournamentProvider = tournamentProvider; + this.participantProvider = participantProvider; + } + + + @Override + public List readCSV(String csvContent) { + final String[] headers = getHeaders(csvContent); + checkHeaders(headers, NAME_HEADER, TOURNAMENT_HEADER, MEMBER1_HEADER, MEMBER2_HEADER, MEMBER3_HEADER, MEMBER4_HEADER, MEMBER5_HEADER, MEMBER6_HEADER, + MEMBER7_HEADER, MEMBER8_HEADER, MEMBER9_HEADER); + final String[] content = getContent(csvContent); + final List teams = new ArrayList<>(); + + final int nameIndex = getHeaderIndex(headers, NAME_HEADER); + final int tournamentIndex = getHeaderIndex(headers, TOURNAMENT_HEADER); + final int member1Index = getHeaderIndex(headers, MEMBER1_HEADER); + final int member2Index = getHeaderIndex(headers, MEMBER2_HEADER); + final int member3Index = getHeaderIndex(headers, MEMBER3_HEADER); + final int member4Index = getHeaderIndex(headers, MEMBER4_HEADER); + final int member5Index = getHeaderIndex(headers, MEMBER5_HEADER); + final int member6Index = getHeaderIndex(headers, MEMBER6_HEADER); + final int member7Index = getHeaderIndex(headers, MEMBER7_HEADER); + final int member8Index = getHeaderIndex(headers, MEMBER8_HEADER); + final int member9Index = getHeaderIndex(headers, MEMBER9_HEADER); + + for (String teamLine : content) { + final Team team = new Team(); + team.setName(getField(teamLine, nameIndex)); + try { + team.setTournament(tournamentProvider.findByName(getField(teamLine, tournamentIndex)).orElseThrow(() + -> new TournamentNotFoundException(this.getClass(), "No tournament with name '" + + getField(teamLine, tournamentIndex) + "' exists."))); + } catch (Exception e) { + KendoTournamentLogger.errorMessage(this.getClass(), e); + } + addMember(teamLine, team, member1Index); + addMember(teamLine, team, member2Index); + addMember(teamLine, team, member3Index); + addMember(teamLine, team, member4Index); + addMember(teamLine, team, member5Index); + addMember(teamLine, team, member6Index); + addMember(teamLine, team, member7Index); + addMember(teamLine, team, member8Index); + addMember(teamLine, team, member9Index); + + //Remove latest null members + for (int i = team.getMembers().size() - 1; i >= 0; i--) { + if (team.getMembers().get(i) != null) { + break; + } + team.getMembers().remove(i); + } + + teams.add(team); + } + return teams; + } + + private void addMember(String teamLine, Team team, int memberIndex) { + if (memberIndex >= 0) { + final String idCard = getField(teamLine, memberIndex); + if (idCard != null && !idCard.isBlank()) { + final Participant participant = participantProvider.findByIdCard(idCard).orElse(null); + if (participant != null) { + team.addMember(participant); + } else { + KendoTournamentLogger.severe(this.getClass().getName(), "Error when inserting CSV from '" + teamLine + "'."); + KendoTournamentLogger.errorMessage(this.getClass(), "No member with id '" + getField(teamLine, memberIndex) + "' on team '" + + team.getName() + "'."); + } + } else { + team.addMember(null); + } + } + } +} diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/exceptions/InvalidCsvFieldException.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/exceptions/InvalidCsvFieldException.java new file mode 100644 index 000000000..7ab9d20b2 --- /dev/null +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/exceptions/InvalidCsvFieldException.java @@ -0,0 +1,51 @@ +package com.softwaremagico.kt.core.exceptions; + +/*- + * #%L + * Kendo Tournament Manager (Core) + * %% + * Copyright (C) 2021 - 2025 Softwaremagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.softwaremagico.kt.logger.ExceptionType; +import com.softwaremagico.kt.logger.LoggedException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.io.Serial; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class InvalidCsvFieldException extends LoggedException { + + private final String header; + + @Serial + private static final long serialVersionUID = 6553482328184416893L; + + public InvalidCsvFieldException(Class clazz, String message, ExceptionType type, String header) { + super(clazz, message, type, HttpStatus.BAD_REQUEST); + this.header = header; + } + + public InvalidCsvFieldException(Class clazz, String message, String header) { + this(clazz, message, ExceptionType.SEVERE, header); + } + + public String getHeader() { + return header; + } +} diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/exceptions/InvalidCsvRowException.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/exceptions/InvalidCsvRowException.java new file mode 100644 index 000000000..20565a121 --- /dev/null +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/exceptions/InvalidCsvRowException.java @@ -0,0 +1,52 @@ +package com.softwaremagico.kt.core.exceptions; + +/*- + * #%L + * Kendo Tournament Manager (Core) + * %% + * Copyright (C) 2021 - 2025 Softwaremagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.softwaremagico.kt.logger.ExceptionType; +import com.softwaremagico.kt.logger.LoggedException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.io.Serial; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class InvalidCsvRowException extends LoggedException { + + private final int numberOfFailedRows; + + @Serial + private static final long serialVersionUID = -980206083113568196L; + + public InvalidCsvRowException(Class clazz, String message, ExceptionType type, int numberOfFailedRows) { + super(clazz, message, type, HttpStatus.BAD_REQUEST); + this.numberOfFailedRows = numberOfFailedRows; + } + + public InvalidCsvRowException(Class clazz, String message, int numberOfFailedRows) { + super(clazz, message, ExceptionType.WARNING, HttpStatus.BAD_REQUEST); + this.numberOfFailedRows = numberOfFailedRows; + } + + public int getNumberOfFailedRows() { + return numberOfFailedRows; + } +} diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/ClubProvider.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/ClubProvider.java index 18ea265ad..f210776c6 100644 --- a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/ClubProvider.java +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/ClubProvider.java @@ -21,11 +21,15 @@ * #L% */ +import com.softwaremagico.kt.persistence.encryption.KeyProperty; import com.softwaremagico.kt.persistence.entities.Club; import com.softwaremagico.kt.persistence.repositories.ClubRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Optional; + @Service public class ClubProvider extends CrudProvider { @@ -37,4 +41,18 @@ public ClubProvider(ClubRepository clubRepository) { public Club add(String name, String country, String city) { return getRepository().save(new Club(name, country, city)); } + + public Optional findBy(String name, String city) { + //If encrypt is enabled. + if (KeyProperty.getDatabaseEncryptionKey() != null && !KeyProperty.getDatabaseEncryptionKey().isBlank()) { + final List clubs = getRepository().findAll(); + for (Club club : clubs) { + if (club.getName().equalsIgnoreCase(name) && club.getCity().equalsIgnoreCase(city)) { + return Optional.of(club); + } + } + return Optional.empty(); + } + return getRepository().findByNameIgnoreCaseAndCityIgnoreCase(name, city); + } } diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/ParticipantProvider.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/ParticipantProvider.java index 4baaf7997..344f536bf 100644 --- a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/ParticipantProvider.java +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/ParticipantProvider.java @@ -23,6 +23,7 @@ import com.softwaremagico.kt.core.controller.models.TemporalToken; import com.softwaremagico.kt.logger.KendoTournamentLogger; +import com.softwaremagico.kt.persistence.encryption.KeyProperty; import com.softwaremagico.kt.persistence.entities.Club; import com.softwaremagico.kt.persistence.entities.Duel; import com.softwaremagico.kt.persistence.entities.Participant; @@ -144,6 +145,19 @@ public Optional findByTokenUsername(String tokenUsername) { return Optional.empty(); } + public Optional findByIdCard(String idCard) { + if (KeyProperty.getDatabaseEncryptionKey() != null && !KeyProperty.getDatabaseEncryptionKey().isBlank()) { + final List participants = getRepository().findAll(); + for (Participant participant : participants) { + if (participant.getIdCard() != null && participant.getIdCard().equalsIgnoreCase(idCard)) { + return Optional.of(participant); + } + } + return Optional.empty(); + } + return getRepository().findByIdCard(idCard); + } + public List getYourWorstNightmare(Participant sourceParticipant) { if (sourceParticipant == null) { return new ArrayList<>(); diff --git a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/TournamentProvider.java b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/TournamentProvider.java index 595a1419d..ed651ab01 100644 --- a/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/TournamentProvider.java +++ b/backend/kendo-tournament-core/src/main/java/com/softwaremagico/kt/core/providers/TournamentProvider.java @@ -26,6 +26,7 @@ import com.softwaremagico.kt.core.tournaments.TournamentHandlerSelector; import com.softwaremagico.kt.core.tournaments.TreeTournamentHandler; import com.softwaremagico.kt.logger.KendoTournamentLogger; +import com.softwaremagico.kt.persistence.encryption.KeyProperty; import com.softwaremagico.kt.persistence.entities.Group; import com.softwaremagico.kt.persistence.entities.Role; import com.softwaremagico.kt.persistence.entities.Team; @@ -57,6 +58,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; @Service public class TournamentProvider extends CrudProvider { @@ -291,4 +293,18 @@ public Tournament findLastByUnlocked() { } return null; } + + public Optional findByName(String name) { + //If encrypt is enabled. + if (KeyProperty.getDatabaseEncryptionKey() != null && !KeyProperty.getDatabaseEncryptionKey().isBlank()) { + final List tournaments = getRepository().findAll(); + for (Tournament tournament : tournaments) { + if (tournament.getName().equalsIgnoreCase(name)) { + return Optional.of(tournament); + } + } + return Optional.empty(); + } + return getRepository().findByName(name); + } } diff --git a/backend/kendo-tournament-core/src/test/java/com/softwaremagico/kt/core/tests/csv/CsvReaderTest.java b/backend/kendo-tournament-core/src/test/java/com/softwaremagico/kt/core/tests/csv/CsvReaderTest.java new file mode 100644 index 000000000..55723915a --- /dev/null +++ b/backend/kendo-tournament-core/src/test/java/com/softwaremagico/kt/core/tests/csv/CsvReaderTest.java @@ -0,0 +1,160 @@ +package com.softwaremagico.kt.core.tests.csv; + +/*- + * #%L + * Kendo Tournament Manager (Core) + * %% + * Copyright (C) 2021 - 2025 SoftwareMagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.softwaremagico.kt.core.controller.CsvController; +import com.softwaremagico.kt.core.controller.TournamentController; +import com.softwaremagico.kt.core.controller.models.ParticipantDTO; +import com.softwaremagico.kt.core.controller.models.TournamentDTO; +import com.softwaremagico.kt.core.exceptions.InvalidCsvFieldException; +import com.softwaremagico.kt.core.providers.ClubProvider; +import com.softwaremagico.kt.core.providers.ParticipantProvider; +import com.softwaremagico.kt.core.providers.TeamProvider; +import com.softwaremagico.kt.persistence.values.TournamentType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +@SpringBootTest +@Test(groups = {"csvReader"}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class CsvReaderTest extends AbstractTestNGSpringContextTests { + + private static final String TOURNAMENT_NAME = "CsvTournament"; + private static final int MEMBERS = 3; + + private static final String ONE_CLUBS_CSV_FILE_PATH = "csv/oneClub.csv"; + private static final String CLUBS_CSV_FILE_PATH = "csv/clubs.csv"; + + private static final String ONE_PARTICIPANT_CSV_FILE_PATH = "csv/oneParticipant.csv"; + private static final String PARTICIPANTS_CSV_FILE_PATH = "csv/participants.csv"; + private static final String INVALID_PARTICIPANTS_CSV_FILE_PATH = "csv/invalidParticipants.csv"; + + private static final String ONE_TEAM_CSV_FILE_PATH = "csv/oneTeam.csv"; + private static final String TEAMS_CSV_FILE_PATH = "csv/teams.csv"; + + @Autowired + private TournamentController tournamentController; + + @Autowired + private CsvController csvController; + + @Autowired + private ClubProvider clubProvider; + + @Autowired + private ParticipantProvider participantProvider; + + @Autowired + private TeamProvider teamProvider; + + private String readCsvFile(String fileName) throws URISyntaxException, IOException { + return new String(Files.readAllBytes(Paths.get(getClass().getClassLoader() + .getResource(fileName).toURI()))); + } + + @BeforeClass + public void prepareTournament1() { + //Create Tournament + tournamentController.create(new TournamentDTO(TOURNAMENT_NAME, 1, MEMBERS, TournamentType.LEAGUE), null, null); + } + + @Test + public void addOneClub() throws URISyntaxException, IOException { + Assert.assertEquals(clubProvider.count(), 0); + csvController.addClubs(readCsvFile(ONE_CLUBS_CSV_FILE_PATH), null); + Assert.assertEquals(clubProvider.count(), 1); + } + + @Test(dependsOnMethods = "addOneClub") + public void addMultiplesClubs() throws URISyntaxException, IOException { + Assert.assertEquals(clubProvider.count(), 1); + csvController.addClubs(readCsvFile(CLUBS_CSV_FILE_PATH), null); + Assert.assertEquals(clubProvider.count(), 8); + } + + @Test(dependsOnMethods = "addMultiplesClubs") + public void addOneParticipant() throws URISyntaxException, IOException { + Assert.assertEquals(participantProvider.count(), 0); + csvController.addParticipants(readCsvFile(ONE_PARTICIPANT_CSV_FILE_PATH), null); + Assert.assertEquals(participantProvider.count(), 3); + } + + @Test(dependsOnMethods = "addOneParticipant") + public void addMultipleParticipant() throws URISyntaxException, IOException { + Assert.assertEquals(participantProvider.count(), 3); + csvController.addParticipants(readCsvFile(PARTICIPANTS_CSV_FILE_PATH), null); + Assert.assertEquals(participantProvider.count(), 18); + } + + @Test(dependsOnMethods = "addOneParticipant") + public void addInvalidParticipant() throws URISyntaxException, IOException { + final List invalidParticipants = csvController.addParticipants(readCsvFile(INVALID_PARTICIPANTS_CSV_FILE_PATH), null); + Assert.assertEquals(invalidParticipants.size(), 3); + } + + @Test(dependsOnMethods = "addMultipleParticipant") + public void addOneTeam() throws URISyntaxException, IOException { + Assert.assertEquals(teamProvider.count(), 0); + csvController.addTeams(readCsvFile(ONE_TEAM_CSV_FILE_PATH), null); + Assert.assertEquals(teamProvider.count(), 1); + Assert.assertEquals(teamProvider.getAll().get(0).getMembers().get(0).getIdCard(), "00000003"); + Assert.assertEquals(teamProvider.getAll().get(0).getMembers().get(1).getIdCard(), "00000001"); + Assert.assertEquals(teamProvider.getAll().get(0).getMembers().get(2).getIdCard(), "00000002"); + } + + @Test(dependsOnMethods = "addOneTeam") + public void addMultipleTeams() throws URISyntaxException, IOException { + Assert.assertEquals(teamProvider.count(), 1); + csvController.addTeams(readCsvFile(TEAMS_CSV_FILE_PATH), null); + Assert.assertEquals(teamProvider.count(), 6); + //Members order is corrected. + Assert.assertEquals(teamProvider.getAll().get(0).getMembers().get(0).getIdCard(), "00000001"); + Assert.assertEquals(teamProvider.getAll().get(0).getMembers().get(1).getIdCard(), "00000002"); + Assert.assertEquals(teamProvider.getAll().get(0).getMembers().get(2).getIdCard(), "00000003"); + } + + @Test(expectedExceptions = InvalidCsvFieldException.class) + public void checkInvalidTeamCSV() throws URISyntaxException, IOException { + csvController.addTeams(readCsvFile(CLUBS_CSV_FILE_PATH), null); + } + + @Test(expectedExceptions = InvalidCsvFieldException.class) + public void checkInvalidClubCSV() throws URISyntaxException, IOException { + csvController.addClubs(readCsvFile(TEAMS_CSV_FILE_PATH), null); + } + + @Test(expectedExceptions = InvalidCsvFieldException.class) + public void checkInvalidParticipantCSV() throws URISyntaxException, IOException { + csvController.addParticipants(readCsvFile(CLUBS_CSV_FILE_PATH), null); + } +} diff --git a/backend/kendo-tournament-core/src/test/resources/csv/clubs.csv b/backend/kendo-tournament-core/src/test/resources/csv/clubs.csv new file mode 100644 index 000000000..e7a646d0e --- /dev/null +++ b/backend/kendo-tournament-core/src/test/resources/csv/clubs.csv @@ -0,0 +1,9 @@ +#Name; Country; City; Address; Phone; Email; Web; +Técnicos de Investigación Aeroterráquea;Spain;Valéncia;;;; +Il Clan dei Camorristi;Italy;Castello D'Aversa; via 1; +32555666777;clan@camorristi.it;https://dei-camorristi.it; +Cobra Kai; USA; Los Angeles; All-Valley 1;;; +Le Club des Gourmets; France; Cassis; +Nihon Bunka Kurabu;Japan; Osaka +Três Grandes; Portugal; Oporto +Trojans; Greece; Minos; +Die Greifen; German; Berlin \ No newline at end of file diff --git a/backend/kendo-tournament-core/src/test/resources/csv/invalidParticipants.csv b/backend/kendo-tournament-core/src/test/resources/csv/invalidParticipants.csv new file mode 100644 index 000000000..20e8c86fa --- /dev/null +++ b/backend/kendo-tournament-core/src/test/resources/csv/invalidParticipants.csv @@ -0,0 +1,4 @@ +#name;lastname;idCard;club;clubCity +Mengano2;López2;10000001;Técnicos de Investigación Aeroterráquea;Cuenca +Fulano2;Férnandez2;10000002;Cobra Kai;Valéncia +Zutano2;Rodríguez2;00000001;Técnicos de Investigación Aeroterráquea;Valéncia \ No newline at end of file diff --git a/backend/kendo-tournament-core/src/test/resources/csv/oneClub.csv b/backend/kendo-tournament-core/src/test/resources/csv/oneClub.csv new file mode 100644 index 000000000..5e056d2c4 --- /dev/null +++ b/backend/kendo-tournament-core/src/test/resources/csv/oneClub.csv @@ -0,0 +1,2 @@ +#Name; Country; City; Address; Phone; Email; Web; +Técnicos de Investigación Aeroterráquea;Spain;Valéncia \ No newline at end of file diff --git a/backend/kendo-tournament-core/src/test/resources/csv/oneParticipant.csv b/backend/kendo-tournament-core/src/test/resources/csv/oneParticipant.csv new file mode 100644 index 000000000..c9b3a364b --- /dev/null +++ b/backend/kendo-tournament-core/src/test/resources/csv/oneParticipant.csv @@ -0,0 +1,4 @@ +#name;lastname;idCard;club;clubCity +Mengano;López;00000001;Técnicos de Investigación Aeroterráquea;Valéncia +Fulano;Férnandez;00000002;Técnicos de Investigación Aeroterráquea;Valéncia +Zutano;Rodríguez;00000003;Técnicos de Investigación Aeroterráquea;Valéncia \ No newline at end of file diff --git a/backend/kendo-tournament-core/src/test/resources/csv/oneTeam.csv b/backend/kendo-tournament-core/src/test/resources/csv/oneTeam.csv new file mode 100644 index 000000000..24ac50050 --- /dev/null +++ b/backend/kendo-tournament-core/src/test/resources/csv/oneTeam.csv @@ -0,0 +1,2 @@ +#name;tournament;member1;member2;member3;member4;member5;member6;member7;member8;member9 +team1;CsvTournament;00000003;00000001;00000002; \ No newline at end of file diff --git a/backend/kendo-tournament-core/src/test/resources/csv/participants.csv b/backend/kendo-tournament-core/src/test/resources/csv/participants.csv new file mode 100644 index 000000000..ab6268ca0 --- /dev/null +++ b/backend/kendo-tournament-core/src/test/resources/csv/participants.csv @@ -0,0 +1,19 @@ +#name;lastname;idCard;club;clubCity +Mengano;López;00000001;Técnicos de Investigación Aeroterráquea;Valéncia +Fulano;Férnandez;00000002;Técnicos de Investigación Aeroterráquea;Valéncia +Zutano;Rodríguez;00000003;Técnicos de Investigación Aeroterráquea;Valéncia +Caio;Fabbro;00000011;Il Clan dei Camorristi;Castello D'Aversa +Tizio;Russo;00000012;Il Clan dei Camorristi;Castello D'Aversa +Sempronio;Colombo;00000013;Il Clan dei Camorristi;Castello D'Aversa +Harry;Evans;00000021;Cobra Kai; Los Angeles +Dick;Taylor;00000022;Cobra Kai; Los Angeles +Tom;Smith;00000023;Cobra Kai; Los Angeles +Jacques;Durand;00000031;Le Club Des Gourmets; Cassis +Paul;Bernard;00000032;Le Club Des Gourmets; Cassis +Pierre;Dupont;00000033;Le Club Des Gourmets;Cassis +Taro;Yamada;00000041;Nihon Bunka Kurabu; Osaka +Hanako;Yamada;00000042;Nihon Bunka Kurabu; Osaka +Hiroko;Suzuki;00000043;Nihon Bunka Kurabu; Osaka +Fernandes;Alemida Costa;00000051; Três Grandes; Oporto +António Silva;Abreu Melo;00000052; Três Grandes; Oporto +Pedro;Lopes Marques;00000053; Três Grandes; Oporto \ No newline at end of file diff --git a/backend/kendo-tournament-core/src/test/resources/csv/teams.csv b/backend/kendo-tournament-core/src/test/resources/csv/teams.csv new file mode 100644 index 000000000..d956f0d31 --- /dev/null +++ b/backend/kendo-tournament-core/src/test/resources/csv/teams.csv @@ -0,0 +1,7 @@ +#name;tournament;member1;member2;member3;member4;member5;member6;member7;member8;member9 +team1;CsvTournament;00000001;00000002;00000003; +team2;CsvTournament;00000011;00000012;00000013; +team3;CsvTournament;00000021;00000022;00000023; +team4;CsvTournament;00000031;00000032;00000033; +team5;CsvTournament;00000041;00000042;00000043; +team6;CsvTournament;00000051;00000052;00000053; \ No newline at end of file diff --git a/backend/kendo-tournament-core/src/test/resources/testng.xml b/backend/kendo-tournament-core/src/test/resources/testng.xml index 94d434b18..034a2a99d 100644 --- a/backend/kendo-tournament-core/src/test/resources/testng.xml +++ b/backend/kendo-tournament-core/src/test/resources/testng.xml @@ -38,6 +38,7 @@ + @@ -77,6 +78,7 @@ + diff --git a/backend/kendo-tournament-logger/pom.xml b/backend/kendo-tournament-logger/pom.xml index 7743cace9..374fca00d 100644 --- a/backend/kendo-tournament-logger/pom.xml +++ b/backend/kendo-tournament-logger/pom.xml @@ -9,7 +9,7 @@ kendo-tournament-backend com.softwaremagico - 2.18.1 + 2.19.0 diff --git a/backend/kendo-tournament-pdf/pom.xml b/backend/kendo-tournament-pdf/pom.xml index 0ab3f40b1..36e858951 100644 --- a/backend/kendo-tournament-pdf/pom.xml +++ b/backend/kendo-tournament-pdf/pom.xml @@ -9,7 +9,7 @@ kendo-tournament-backend com.softwaremagico - 2.18.1 + 2.19.0 diff --git a/backend/kendo-tournament-persistence/pom.xml b/backend/kendo-tournament-persistence/pom.xml index f27bb0e1f..a8ccb5e56 100644 --- a/backend/kendo-tournament-persistence/pom.xml +++ b/backend/kendo-tournament-persistence/pom.xml @@ -9,7 +9,7 @@ kendo-tournament-backend com.softwaremagico - 2.18.1 + 2.19.0 diff --git a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/entities/Participant.java b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/entities/Participant.java index e583ad93c..a5cc12007 100644 --- a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/entities/Participant.java +++ b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/entities/Participant.java @@ -126,7 +126,9 @@ public String getIdCard() { } public void setIdCard(String value) { - idCard = value.replace("-", "").replace(" ", "").trim().toUpperCase(); + if (value != null) { + idCard = value.replace("-", "").replace(" ", "").trim().toUpperCase(); + } } public boolean isValid() { diff --git a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/entities/Tournament.java b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/entities/Tournament.java index c2883a0c8..74300d482 100644 --- a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/entities/Tournament.java +++ b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/entities/Tournament.java @@ -51,7 +51,7 @@ public class Tournament extends Element implements IName { public static final int DEFAULT_DURATION = 180; - @Column(name = "name", nullable = false) + @Column(name = "name", nullable = false, unique = true) @Convert(converter = StringCryptoConverter.class) private String name; diff --git a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/ClubRepository.java b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/ClubRepository.java index 241a8f21a..bd0f95d2e 100644 --- a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/ClubRepository.java +++ b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/ClubRepository.java @@ -26,8 +26,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository @Transactional public interface ClubRepository extends JpaRepository { + Optional findByNameIgnoreCaseAndCityIgnoreCase(String name, String city); } diff --git a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/ParticipantRepository.java b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/ParticipantRepository.java index 96e7c479b..d8eafedfe 100644 --- a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/ParticipantRepository.java +++ b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/ParticipantRepository.java @@ -96,4 +96,6 @@ List findParticipantsWithRoleNotInTournaments(@Param("tournament") Optional findByTemporalToken(String temporalToken); Optional findByToken(String token); + + Optional findByIdCard(String idCard); } diff --git a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/TournamentRepository.java b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/TournamentRepository.java index 3418bb1c0..ea3694846 100644 --- a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/TournamentRepository.java +++ b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/persistence/repositories/TournamentRepository.java @@ -29,6 +29,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @Repository @Transactional @@ -39,4 +40,6 @@ public interface TournamentRepository extends JpaRepository List findByCreatedAtLessThan(LocalDateTime createdAt); List findByLocked(boolean locked); + + Optional findByName(String name); } diff --git a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/utils/StringUtils.java b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/utils/StringUtils.java index 4a24c5bee..8948c75ba 100644 --- a/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/utils/StringUtils.java +++ b/backend/kendo-tournament-persistence/src/main/java/com/softwaremagico/kt/utils/StringUtils.java @@ -35,6 +35,9 @@ private StringUtils() { } public static String setCase(String value) { + if (value == null) { + return null; + } final StringBuilder caseString = new StringBuilder(); final String[] data = value.split(" "); for (final String datum : data) { diff --git a/backend/kendo-tournament-rest/pom.xml b/backend/kendo-tournament-rest/pom.xml index d7c32b9b2..84de2d161 100644 --- a/backend/kendo-tournament-rest/pom.xml +++ b/backend/kendo-tournament-rest/pom.xml @@ -9,7 +9,7 @@ kendo-tournament-backend com.softwaremagico - 2.18.1 + 2.19.0 diff --git a/backend/kendo-tournament-rest/src/main/java/com/softwaremagico/kt/rest/exceptions/ExceptionControllerAdvice.java b/backend/kendo-tournament-rest/src/main/java/com/softwaremagico/kt/rest/exceptions/ExceptionControllerAdvice.java index a72565653..655d6b006 100644 --- a/backend/kendo-tournament-rest/src/main/java/com/softwaremagico/kt/rest/exceptions/ExceptionControllerAdvice.java +++ b/backend/kendo-tournament-rest/src/main/java/com/softwaremagico/kt/rest/exceptions/ExceptionControllerAdvice.java @@ -22,6 +22,8 @@ */ import com.softwaremagico.kt.core.exceptions.InvalidChallengeDistanceException; +import com.softwaremagico.kt.core.exceptions.InvalidCsvFieldException; +import com.softwaremagico.kt.core.exceptions.InvalidCsvRowException; import com.softwaremagico.kt.core.exceptions.InvalidFightException; import com.softwaremagico.kt.core.exceptions.InvalidGroupException; import com.softwaremagico.kt.core.exceptions.LevelNotFinishedException; @@ -186,6 +188,20 @@ public ResponseEntity invalidFightException(Exception ex) { return new ResponseEntity<>(new ErrorResponse(ex.getMessage(), "invalid_fight", ex), HttpStatus.BAD_REQUEST); } + @ExceptionHandler(InvalidCsvFieldException.class) + public ResponseEntity invalidCsvFieldException(Exception ex) { + RestServerExceptionLogger.errorMessage(this.getClass().getName(), ex); + return new ResponseEntity<>(new ErrorResponse(ex.getMessage(), "Invalid field: " + ((InvalidCsvFieldException) ex).getHeader(), ex), + HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(InvalidCsvRowException.class) + public ResponseEntity invalidCsvRowException(Exception ex) { + RestServerExceptionLogger.errorMessage(this.getClass().getName(), ex); + return new ResponseEntity<>(new ErrorResponse(ex.getMessage(), "Elements failed: " + ((InvalidCsvRowException) ex).getNumberOfFailedRows(), ex), + HttpStatus.BAD_REQUEST); + } + @Override protected ResponseEntity handleMethodArgumentNotValid( diff --git a/backend/kendo-tournament-rest/src/main/java/com/softwaremagico/kt/rest/services/CsvServices.java b/backend/kendo-tournament-rest/src/main/java/com/softwaremagico/kt/rest/services/CsvServices.java new file mode 100644 index 000000000..60e9947ea --- /dev/null +++ b/backend/kendo-tournament-rest/src/main/java/com/softwaremagico/kt/rest/services/CsvServices.java @@ -0,0 +1,95 @@ +package com.softwaremagico.kt.rest.services; + +/*- + * #%L + * Kendo Tournament Manager (Rest) + * %% + * Copyright (C) 2021 - 2025 Softwaremagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.softwaremagico.kt.core.controller.CsvController; +import com.softwaremagico.kt.core.controller.models.ClubDTO; +import com.softwaremagico.kt.core.controller.models.ParticipantDTO; +import com.softwaremagico.kt.core.controller.models.TeamDTO; +import com.softwaremagico.kt.core.exceptions.InvalidCsvFieldException; +import com.softwaremagico.kt.core.exceptions.InvalidCsvRowException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@RestController +@RequestMapping("/csv") +public class CsvServices { + + private final CsvController csvController; + + public CsvServices(CsvController csvController) { + this.csvController = csvController; + } + + + @PreAuthorize("hasAnyAuthority(@securityService.editorPrivilege, @securityService.adminPrivilege)") + @Operation(summary = "Add clubs from a CSV file. Returns any failed club attempt.", security = @SecurityRequirement(name = "bearerAuth")) + @PostMapping(value = "/clubs", produces = MediaType.APPLICATION_JSON_VALUE) + public List addClubs(@RequestParam("file") MultipartFile file, + Authentication authentication, HttpServletRequest request) throws IOException, InvalidCsvFieldException { + final List failedClubs = csvController.addClubs(new String(file.getBytes(), StandardCharsets.UTF_8), authentication.getName()); + if (!failedClubs.isEmpty()) { + throw new InvalidCsvRowException(this.getClass(), "Some clubs have not been inserted correctly!", failedClubs.size()); + } + return failedClubs; + } + + + @PreAuthorize("hasAnyAuthority(@securityService.editorPrivilege, @securityService.adminPrivilege)") + @Operation(summary = "Add participants from a CSV file. Returns any failed participant attempt.", security = @SecurityRequirement(name = "bearerAuth")) + @PostMapping(value = "/participants", produces = MediaType.APPLICATION_JSON_VALUE) + public List addParticipants(@RequestParam("file") MultipartFile file, + Authentication authentication, HttpServletRequest request) throws IOException, InvalidCsvFieldException { + final List failedParticipants = csvController.addParticipants(new String(file.getBytes(), StandardCharsets.UTF_8), + authentication.getName()); + if (!failedParticipants.isEmpty()) { + throw new InvalidCsvRowException(this.getClass(), "Some participants have not been inserted correctly!", failedParticipants.size()); + } + return failedParticipants; + } + + + @PreAuthorize("hasAnyAuthority(@securityService.editorPrivilege, @securityService.adminPrivilege)") + @Operation(summary = "Add teams from a CSV file. Returns any failed team attempt.", security = @SecurityRequirement(name = "bearerAuth")) + @PostMapping(value = "/teams", produces = MediaType.APPLICATION_JSON_VALUE) + public List addTeams(@RequestParam("file") MultipartFile file, + Authentication authentication, HttpServletRequest request) throws IOException, InvalidCsvFieldException { + final List failedTeams = csvController.addTeams(new String(file.getBytes(), StandardCharsets.UTF_8), authentication.getName()); + if (!failedTeams.isEmpty()) { + throw new InvalidCsvRowException(this.getClass(), "Some teams have not been inserted correctly!", failedTeams.size()); + } + return failedTeams; + } +} diff --git a/backend/kendo-tournament-rest/src/test/java/com/softwaremagico/kt/rest/CsvServicesTest.java b/backend/kendo-tournament-rest/src/test/java/com/softwaremagico/kt/rest/CsvServicesTest.java new file mode 100644 index 000000000..265f55cf0 --- /dev/null +++ b/backend/kendo-tournament-rest/src/test/java/com/softwaremagico/kt/rest/CsvServicesTest.java @@ -0,0 +1,158 @@ +package com.softwaremagico.kt.rest; + +/*- + * #%L + * Kendo Tournament Manager (Rest) + * %% + * Copyright (C) 2021 - 2025 Softwaremagico + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.softwaremagico.kt.core.controller.models.ClubDTO; +import com.softwaremagico.kt.persistence.repositories.ClubRepository; +import com.softwaremagico.kt.rest.controllers.AuthenticatedUserController; +import com.softwaremagico.kt.rest.security.dto.AuthRequest; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@Test(groups = "csvServices") +public class CsvServicesTest extends AbstractTestNGSpringContextTests { + + private static final String USER_FIRST_NAME = "Test"; + private static final String USER_LAST_NAME = "User"; + + private static final String USER_NAME = USER_FIRST_NAME + "." + USER_LAST_NAME; + private static final String USER_PASSWORD = "password"; + private static final String[] USER_ROLES = new String[]{"admin", "viewer"}; + + private static final String ONE_CLUBS_CSV_FILE_PATH = "csv/oneClub.csv"; + private static final String CLUBS_CSV_FILE_PATH = "csv/clubs.csv"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private AuthenticatedUserController authenticatedUserController; + + @Autowired + private ClubRepository clubRepository; + + private String jwtToken; + + private String toJson(T object) throws JsonProcessingException { + return objectMapper.writeValueAsString(object); + } + + private T fromJson(String payload, Class clazz) throws IOException { + return objectMapper.readValue(payload, clazz); + } + + @BeforeClass + public void setUp() throws Exception { + authenticatedUserController.createUser(null, USER_NAME, USER_FIRST_NAME, USER_LAST_NAME, USER_PASSWORD, USER_ROLES); + + AuthRequest request = new AuthRequest(); + request.setUsername(USER_NAME); + request.setPassword(USER_PASSWORD); + + MvcResult createResult = this.mockMvc + .perform(post("/auth/public/login") + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(request)) + .with(csrf())) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.header().exists(HttpHeaders.AUTHORIZATION)) + .andReturn(); + + jwtToken = createResult.getResponse().getHeader(HttpHeaders.AUTHORIZATION); + Assert.assertNotNull(jwtToken); + } + + @Test + public void uploadClubs() throws Exception { + Assert.assertNotNull(jwtToken); + + final byte[] bytes = Files.readAllBytes(Paths.get(getClass().getClassLoader() + .getResource(ONE_CLUBS_CSV_FILE_PATH).toURI())); + + MvcResult createResult = this.mockMvc + .perform(multipart("/csv/clubs") + .file("file", bytes) + .header("Authorization", "Bearer " + jwtToken) + .with(csrf())) + .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) + .andReturn(); + + //One is malformed, so it is returned as an error. + final List clubDTO = Arrays.asList(fromJson(createResult.getResponse().getContentAsString(), ClubDTO[].class)); + Assert.assertEquals(clubDTO.size(), 0); + } + + @Test + public void uploadInvalidClubs() throws Exception { + Assert.assertNotNull(jwtToken); + + final byte[] bytes = Files.readAllBytes(Paths.get(getClass().getClassLoader() + .getResource(CLUBS_CSV_FILE_PATH).toURI())); + + this.mockMvc + .perform(multipart("/csv/clubs") + .file("file", bytes) + .header("Authorization", "Bearer " + jwtToken) + .with(csrf())) + .andExpect(MockMvcResultMatchers.status().isBadRequest()) + .andReturn(); + } + + + @AfterClass(alwaysRun = true) + public void cleanUp() { + clubRepository.deleteAll(); + authenticatedUserController.deleteAll(); + } +} diff --git a/backend/kendo-tournament-rest/src/test/resources/csv/clubs.csv b/backend/kendo-tournament-rest/src/test/resources/csv/clubs.csv new file mode 100644 index 000000000..7d9dd9b45 --- /dev/null +++ b/backend/kendo-tournament-rest/src/test/resources/csv/clubs.csv @@ -0,0 +1,3 @@ +#Name; Country; City; Address; Phone; Email; Web; +Técnicos de Investigación Aeroterráquea;Spain;Valéncia +Il Clan dei Camorristi;Italy \ No newline at end of file diff --git a/backend/kendo-tournament-rest/src/test/resources/csv/oneClub.csv b/backend/kendo-tournament-rest/src/test/resources/csv/oneClub.csv new file mode 100644 index 000000000..5e056d2c4 --- /dev/null +++ b/backend/kendo-tournament-rest/src/test/resources/csv/oneClub.csv @@ -0,0 +1,2 @@ +#Name; Country; City; Address; Phone; Email; Web; +Técnicos de Investigación Aeroterráquea;Spain;Valéncia \ No newline at end of file diff --git a/backend/kendo-tournament-rest/src/test/resources/testng.xml b/backend/kendo-tournament-rest/src/test/resources/testng.xml index 669f63ec5..e7cda78aa 100644 --- a/backend/kendo-tournament-rest/src/test/resources/testng.xml +++ b/backend/kendo-tournament-rest/src/test/resources/testng.xml @@ -19,6 +19,7 @@ + @@ -31,6 +32,7 @@ + diff --git a/backend/pom.xml b/backend/pom.xml index efd541b96..af5f48bef 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.softwaremagico kendo-tournament-backend - 2.18.1 + 2.19.0 Kendo Tournament Manager pom diff --git a/docker-examples/local-build/.env b/docker-examples/local-build/.env index d0911cf5d..233437a15 100644 --- a/docker-examples/local-build/.env +++ b/docker-examples/local-build/.env @@ -2,7 +2,7 @@ COMPOSE_PROJECT_NAME=kendo_tournament machine_domain=localhost:8080 email=myemail@domain.com timezone=Europe/Madrid -version=v2.18.1 +version=v2.19.0 release=${version} #Frontend diff --git a/docker-examples/localhost-with-reverse-proxy/.env b/docker-examples/localhost-with-reverse-proxy/.env index e47f5bfa4..1aa28e89b 100644 --- a/docker-examples/localhost-with-reverse-proxy/.env +++ b/docker-examples/localhost-with-reverse-proxy/.env @@ -2,8 +2,8 @@ COMPOSE_PROJECT_NAME=kendo_tournament machine_domain=localhost email=myemail@domain.com timezone=Europe/Madrid -release=v2.18.1 -version=2.18.1 +release=v2.19.0 +version=2.19.0 #Frontend frontend_path=/kendo-tournament-frontend diff --git a/docker-examples/localhost-without-reverse-proxy/.env b/docker-examples/localhost-without-reverse-proxy/.env index 60bf842d7..3e58d0f51 100644 --- a/docker-examples/localhost-without-reverse-proxy/.env +++ b/docker-examples/localhost-without-reverse-proxy/.env @@ -2,7 +2,7 @@ COMPOSE_PROJECT_NAME=kendo_tournament machine_domain=localhost email=myemail@domain.com timezone=Europe/Madrid -version=v2.18.1 +version=v2.19.0 release=${version} #Frontend diff --git a/docker/.env b/docker/.env index 3439a5fab..fd9cc54ee 100644 --- a/docker/.env +++ b/docker/.env @@ -2,7 +2,7 @@ COMPOSE_PROJECT_NAME=kendo_tournament machine_domain=localhost email=myemail@domain.com timezone=Europe/Madrid -version=v2.18.1 +version=v2.19.0 release=${version} #Frontend diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 97e8e622a..b89d36a8b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "2.18.1", + "version": "2.19.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "2.18.1", + "version": "2.19.0", "dependencies": { "@angular/animations": "^15.2.9", "@angular/cdk": "^14.2.7", diff --git a/frontend/package.json b/frontend/package.json index f83fe8ecc..41701ffa1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "2.18.1", + "version": "2.19.0", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/frontend/src/app/components/icons.ts b/frontend/src/app/components/icons.ts index b9aae6286..f18c4e382 100644 --- a/frontend/src/app/components/icons.ts +++ b/frontend/src/app/components/icons.ts @@ -42,7 +42,8 @@ export class IconModule { .addSvgIcon("yourWorstNightmare", this.setPath(`${this.path}/yourWorstNightmare.svg`)) .addSvgIcon("youAreTheWorstNightmareOf", this.setPath(`${this.path}/youAreTheWorstNightmareOf.svg`)) .addSvgIcon("sorted", this.setPath(`${this.path}/sorted.svg`)) - .addSvgIcon("whistle", this.setPath(`${this.path}/whistle.svg`)); + .addSvgIcon("whistle", this.setPath(`${this.path}/whistle.svg`)) + .addSvgIcon("csv-file-small", this.setPath(`${this.path}/csv-file-small.svg`)); } private setPath(url: string): SafeResourceUrl { diff --git a/frontend/src/app/services/csv-service.ts b/frontend/src/app/services/csv-service.ts new file mode 100644 index 000000000..7d7a30146 --- /dev/null +++ b/frontend/src/app/services/csv-service.ts @@ -0,0 +1,77 @@ +import {Injectable} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import {EnvironmentService} from "../environment.service"; +import {MessageService} from "./message.service"; +import {LoggerService} from "./logger.service"; +import {LoginService} from "./login.service"; +import {SystemOverloadService} from "./notifications/system-overload.service"; +import {Club} from "../models/club"; +import {Observable} from "rxjs"; +import {catchError, tap} from "rxjs/operators"; +import {Participant} from "../models/participant"; +import {Team} from "../models/team"; + +@Injectable({ + providedIn: 'root' +}) +export class CsvService { + + private baseUrl: string = this.environmentService.getBackendUrl() + '/csv'; + + constructor(private http: HttpClient, private environmentService: EnvironmentService, private messageService: MessageService, + private loggerService: LoggerService, public loginService: LoginService, + private systemOverloadService: SystemOverloadService) { + } + + + addClubs(file: File): Observable { + this.systemOverloadService.isBusy.next(true); + let url: string = `${this.baseUrl}/clubs`; + const formData = new FormData(); + formData.append("file", file); + formData.append("reportProgress", "true"); + return this.http.post(url, formData) + .pipe( + tap({ + next: (_clubs: Club[]) => this.loggerService.info(`adding clubs as csv`), + error: () => this.systemOverloadService.isBusy.next(false), + complete: () => this.systemOverloadService.isBusy.next(false), + }) + ); + } + + + addParticipants(file: File): Observable { + this.systemOverloadService.isBusy.next(true); + let url: string = `${this.baseUrl}/participants`; + const formData = new FormData(); + formData.append("file", file); + formData.append("reportProgress", "true"); + return this.http.post(url, formData) + .pipe( + tap({ + next: (_participants: Participant[]) => this.loggerService.info(`adding participants as csv`), + error: () => this.systemOverloadService.isBusy.next(false), + complete: () => this.systemOverloadService.isBusy.next(false), + }) + ); + } + + + addTeams(file: File): Observable { + this.systemOverloadService.isBusy.next(true); + let url: string = `${this.baseUrl}/teams`; + const formData = new FormData(); + formData.append("file", file); + formData.append("reportProgress", "true"); + return this.http.post(url, formData) + .pipe( + tap({ + next: (_teams: Team[]) => this.loggerService.info(`adding teams as csv`), + error: () => this.systemOverloadService.isBusy.next(false), + complete: () => this.systemOverloadService.isBusy.next(false), + }) + ); + } + +} diff --git a/frontend/src/app/services/message.service.ts b/frontend/src/app/services/message.service.ts index e84c81172..8950250cd 100644 --- a/frontend/src/app/services/message.service.ts +++ b/frontend/src/app/services/message.service.ts @@ -18,6 +18,8 @@ export class MessageService implements OnDestroy { private messageSubscription: Subscription; + private snackBarActive: boolean = false; + constructor(public snackBar: MatSnackBar, private translateService: TranslateService, private loggerService: LoggerService, private rxStompService: RxStompService, private environmentService: EnvironmentService) { @@ -58,12 +60,19 @@ export class MessageService implements OnDestroy { } private openSnackBar(message: string, cssClass: string, duration: number, action?: string): void { - this.snackBar.open(this.translateService.instant(message), action, { - duration: duration, - panelClass: [cssClass, 'message-service'], - verticalPosition: 'top', - horizontalPosition: 'right' - }); + if (!this.snackBarActive) { + this.snackBarActive = true; + const snackBarRef = this.snackBar.open(this.translateService.instant(message), action, { + duration: duration, + panelClass: [cssClass, 'message-service'], + verticalPosition: 'top', + horizontalPosition: 'right' + }); + snackBarRef.afterDismissed().subscribe(() => { + //Show only one message and not overlap if one already exists. + this.snackBarActive = false; + }); + } } diff --git a/frontend/src/app/services/rbac/rbac.activity.ts b/frontend/src/app/services/rbac/rbac.activity.ts index d2ce9173b..48e50e944 100644 --- a/frontend/src/app/services/rbac/rbac.activity.ts +++ b/frontend/src/app/services/rbac/rbac.activity.ts @@ -2,6 +2,7 @@ export enum RbacActivity { READ_ALL_PARTICIPANTS = 'READ_ALL_PARTICIPANTS', READ_ONE_PARTICIPANT = 'READ_ONE_PARTICIPANT', CREATE_PARTICIPANT = 'CREATE_PARTICIPANT', + CREATE_PARTICIPANT_CSV = 'CREATE_PARTICIPANT_CSV', EDIT_PARTICIPANT = 'EDIT_PARTICIPANT', SEE_PICTURE = 'SEE_PICTURE', TAKE_PICTURE = 'TAKE_PICTURE', @@ -12,6 +13,7 @@ export enum RbacActivity { READ_ALL_CLUBS = 'READ_ALL_CLUBS', READ_ONE_CLUB = 'READ_ONE_CLUB', CREATE_CLUB = 'CREATE_CLUB', + CREATE_CLUB_CSV = 'CREATE_CLUB_CSV', EDIT_CLUB = 'EDIT_CLUB', DELETE_CLUB = 'DELETE_CLUB', @@ -39,6 +41,7 @@ export enum RbacActivity { READ_ALL_TEAMS = 'READ_ALL_TEAMS', READ_ONE_TEAM = 'READ_ONE_TEAM', CREATE_TEAM = 'CREATE_TEAM', + CREATE_TEAM_CSV = 'CREATE_TEAM_CSV', EDIT_TEAM = 'EDIT_TEAM', DELETE_TEAM = 'DELETE_TEAM', diff --git a/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.html b/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.html index c96e03328..ba12f03c0 100644 --- a/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.html +++ b/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.html @@ -1,3 +1,14 @@ +

{{title}}

diff --git a/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.scss b/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.scss index 8e43328b8..9d2543da1 100644 --- a/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.scss +++ b/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.scss @@ -5,6 +5,7 @@ .mat-dialog-content { overflow: visible; + position: relative; } .mat-form-field { @@ -14,3 +15,7 @@ .mat-raised-button { width: 150px; } + +.floating-button { + float: right; +} diff --git a/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.ts b/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.ts index 7d4d31e06..418fd4967 100644 --- a/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.ts +++ b/frontend/src/app/views/club-list/club-dialog-box/club-dialog-box.component.ts @@ -7,6 +7,9 @@ import {RbacService} from "../../../services/rbac/rbac.service"; import {UntypedFormControl, UntypedFormGroup, Validators} from "@angular/forms"; import {RbacActivity} from "../../../services/rbac/rbac.activity"; import {InputLimits} from "../../../utils/input-limits"; +import {CsvService} from "../../../services/csv-service"; +import {MessageService} from "../../../services/message.service"; +import {TranslateService} from "@ngx-translate/core"; @Component({ selector: 'app-club-dialog-box', @@ -38,7 +41,8 @@ export class ClubDialogBoxComponent extends RbacBasedComponent { registerForm: UntypedFormGroup; constructor( - public dialogRef: MatDialogRef, rbacService: RbacService, + public dialogRef: MatDialogRef, rbacService: RbacService, public csvService: CsvService, + public messageService: MessageService, private translateService: TranslateService, //@Optional() is used to prevent error if no data is passed @Optional() @Inject(MAT_DIALOG_DATA) public data: { title: string, action: Action, entity: Club }) { super(rbacService); @@ -95,4 +99,25 @@ export class ClubDialogBoxComponent extends RbacBasedComponent { this.dialogRef.close({action: Action.Cancel}); } + handleFileInput(event: Event) { + const element = event.currentTarget as HTMLInputElement; + let fileList: FileList | null = element.files; + if (fileList) { + const file: File | null = fileList.item(0); + if (file) { + this.csvService.addClubs(file).subscribe(_clubs => { + if(_clubs.length==0) { + this.messageService.infoMessage('clubStored'); + //We cancel action or will be saved later again. + this.dialogRef.close({action: Action.Cancel}); + }else{ + const parameters: object = {element: _clubs[0].name}; + this.translateService.get('failedOnCsvField', parameters).subscribe((message: string): void => { + this.messageService.errorMessage(message); + }); + } + }); + } + } + } } diff --git a/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.html b/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.html index a3cbaee4a..727f2735a 100644 --- a/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.html +++ b/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.html @@ -1,3 +1,14 @@ +

{{title}}

diff --git a/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.scss b/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.scss index 2c90326f7..e6c35b9ed 100644 --- a/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.scss +++ b/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.scss @@ -5,6 +5,7 @@ .mat-dialog-content { overflow: visible; + position: relative; } .mat-form-field { @@ -38,7 +39,6 @@ } .floating-button { - position: absolute; - margin-left: 120px; - margin-top: -150px; + float: right; } + diff --git a/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.ts b/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.ts index 320c694dd..82e08ddcf 100644 --- a/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.ts +++ b/frontend/src/app/views/participant-list/participant-dialog-box/participant-dialog-box.component.ts @@ -13,6 +13,8 @@ import {MessageService} from "../../../services/message.service"; import {ParticipantImage} from "../../../models/participant-image.model"; import {RbacActivity} from "../../../services/rbac/rbac.activity"; import {InputLimits} from "../../../utils/input-limits"; +import {CsvService} from "../../../services/csv-service"; +import {TranslateService} from "@ngx-translate/core"; @Component({ selector: 'app-participant-dialog-box', @@ -39,13 +41,13 @@ export class ParticipantDialogBoxComponent extends RbacBasedComponent implements participantPicture: string | undefined; constructor( - public dialogRef: MatDialogRef, rbacService: RbacService, + public dialogRef: MatDialogRef, rbacService: RbacService, public csvService: CsvService, @Optional() @Inject(MAT_DIALOG_DATA) public data: { title: string, action: Action, entity: Participant, clubs: Club[] - }, public dialog: MatDialog, + }, public dialog: MatDialog, private translateService: TranslateService, private pictureUpdatedService: PictureUpdatedService, private fileService: FileService, private messageService: MessageService) { super(rbacService); this.participant = data.entity; @@ -142,4 +144,27 @@ export class ParticipantDialogBoxComponent extends RbacBasedComponent implements this.participantPicture = undefined; }); } + + + handleFileInput(event: Event) { + const element = event.currentTarget as HTMLInputElement; + let fileList: FileList | null = element.files; + if (fileList) { + const file: File | null = fileList.item(0); + if (file) { + this.csvService.addParticipants(file).subscribe(_participants => { + if (_participants.length == 0) { + this.messageService.infoMessage('infoParticipantStored'); + //We cancel action or will be saved later again. + this.dialogRef.close({action: Action.Cancel}); + } else { + const parameters: object = {element: _participants[0].name}; + this.translateService.get('failedOnCsvField', parameters).subscribe((message: string): void => { + this.messageService.errorMessage(message); + }); + } + }); + } + } + } } diff --git a/frontend/src/app/views/tournament-list/tournament-teams/tournament-teams.component.html b/frontend/src/app/views/tournament-list/tournament-teams/tournament-teams.component.html index 1d156806c..b733f8e6b 100644 --- a/frontend/src/app/views/tournament-list/tournament-teams/tournament-teams.component.html +++ b/frontend/src/app/views/tournament-list/tournament-teams/tournament-teams.component.html @@ -1,5 +1,16 @@
+
{ + if (_teams.length == 0) { + this.messageService.infoMessage('teamStored'); + //We cancel action or will be saved later again. + this.dialogRef.close({action: Action.Cancel}); + } else { + const parameters: object = {element: _teams[0].name}; + this.translateService.get('failedOnCsvField', parameters).subscribe((message: string): void => { + this.messageService.errorMessage(message); + }); + } + }); + } + } + } } diff --git a/frontend/src/assets/i18n/ca.json b/frontend/src/assets/i18n/ca.json index 0097256f6..9ee180e54 100644 --- a/frontend/src/assets/i18n/ca.json +++ b/frontend/src/assets/i18n/ca.json @@ -746,5 +746,6 @@ "lightMode": "Mode Clar", "newVersionAvailable": "La versió '{{newVersion}}' està disponible! La versió actual és '{{currentVersion}}'.", "input_data_is_invalid": "Les dades proporcionades no són vàlides!", - "refreshStructure": "Corrige la estructura de los grupos que no tienen ningún combate asignado." + "refreshStructure": "Corrige la estructura de los grupos que no tienen ningún combate asignado.", + "failedOnCsvField": "Error a l'element {{element}}. L'element no s'ha salvat correctament." } diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index a2401d0ae..cbb357405 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -724,5 +724,6 @@ "lightMode": "Lichte Modus", "newVersionAvailable": "Version '{{newVersion}}' ist verfügbar! Aktuelle Version ist '{{currentVersion}}'.", "input_data_is_invalid": "Die angegebenen Daten sind ungültig!", - "refreshStructure": "Korrigieren Sie die Struktur der Gruppen, denen keine Kampfeinsätze zugewiesen sind." + "refreshStructure": "Korrigieren Sie die Struktur der Gruppen, denen keine Kampfeinsätze zugewiesen sind.", + "failedOnCsvField": "Fehler im Element {{element}}. Das Element wurde nicht korrekt gespeichert." } diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 88f263be8..6b3734311 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -296,7 +296,7 @@ "wizard": "Wizard", "confirm": "Confirm", "deleteFightsWarning": "This action will erase all previous matches stored for this tournament. Are you sure you want to continue?", - "deleteFightWarning": "This action will delete the selected match ({{team1}} vs {{team2}}). Are you sure you want to continue?", + "deleteFightWarningdeleteFightWarning": "This action will delete the selected match ({{team1}} vs {{team2}}). Are you sure you want to continue?", "fightsDeleted": "The matches have been deleted!", "hits": "Score", "fights": "Matches", @@ -741,5 +741,6 @@ "lightMode": "Light Mode", "newVersionAvailable": "Version '{{newVersion}}' is available! Current version is '{{currentVersion}}'", "input_data_is_invalid": "Provided data are invalid!", - "refreshStructure": "Correct the structure of groups that do not have any assigned fight." + "refreshStructure": "Correct the structure of groups that do not have any assigned fight.", + "failedOnCsvField": "Error on element {{element}}. Element not saved correctly." } diff --git a/frontend/src/assets/i18n/es.json b/frontend/src/assets/i18n/es.json index 3dca1bf35..e54d10dd0 100644 --- a/frontend/src/assets/i18n/es.json +++ b/frontend/src/assets/i18n/es.json @@ -742,5 +742,6 @@ "lightMode": "Modo Claro", "newVersionAvailable": "¡La versión '{{newVersion}}' está disponible! La versión actual es '{{currentVersion}}'.", "input_data_is_invalid": "¡Los datos proporcionados no son válidos!", - "refreshStructure": "Corrige la estructura de los grupos que no tienen ningún combate asignado." + "refreshStructure": "Corrige la estructura de los grupos que no tienen ningún combate asignado.", + "failedOnCsvField": "Error en el elemento {{element}}. El elemento no se guardó correctamente." } diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 5a019af68..5011fed48 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -730,5 +730,6 @@ "lightMode": "Modalità Chiara", "newVersionAvailable": "La versione '{{newVersion}}' è disponibile! La versione attuale è '{{currentVersion}}'.", "input_data_is_invalid": "I dati forniti non sono validi!", - "refreshStructure": "Correggere la struttura dei gruppi a cui non è assegnato alcun combattimento." + "refreshStructure": "Correggere la struttura dei gruppi a cui non è assegnato alcun combattimento.", + "failedOnCsvField": "Errore nell'elemento {{element}}. L'elemento non è stato salvato correttamente." } diff --git a/frontend/src/assets/i18n/nl.json b/frontend/src/assets/i18n/nl.json index 79fc386a6..01562e93a 100644 --- a/frontend/src/assets/i18n/nl.json +++ b/frontend/src/assets/i18n/nl.json @@ -724,5 +724,6 @@ "lightMode": "Heller Modus", "newVersionAvailable": "Versie '{{newVersion}}' is beschikbaar! Huidige versie is '{{currentVersion}}'.", "input_data_is_invalid": "De opgegeven gegevens zijn ongeldig!", - "refreshStructure": "Verbeter de structuur van groepen waaraan geen gevechtstaken zijn toegewezen." + "refreshStructure": "Verbeter de structuur van groepen waaraan geen gevechtstaken zijn toegewezen.", + "failedOnCsvField": "Fout in element {{element}}. Het element is niet correct opgeslagen." } diff --git a/frontend/src/assets/icons/csv-file-small.svg b/frontend/src/assets/icons/csv-file-small.svg new file mode 100644 index 000000000..1cbdf6d82 --- /dev/null +++ b/frontend/src/assets/icons/csv-file-small.svg @@ -0,0 +1,39 @@ + + + + + +