1
0
Fork 0
mirror of https://codeberg.org/beerbrawl/beerbrawl.git synced 2024-09-22 21:20:52 +02:00

fix(#15): add tournament team delete endpoint

This commit is contained in:
motzik 2024-05-25 10:45:30 +02:00
parent e871ddd874
commit 136b2f1e53
6 changed files with 162 additions and 78 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# db files
/database/db.mv.db
/database/db.trace.db
/database/db.lock.db
# logs
/log

View file

@ -51,8 +51,8 @@ public class TournamentEndpoint {
@Autowired
public TournamentEndpoint(TournamentService tournamentService,
TournamentMapper tournamentMapper,
TeamMapper teamMapper) {
TournamentMapper tournamentMapper,
TeamMapper teamMapper) {
this.tournamentService = tournamentService;
this.tournamentMapper = tournamentMapper;
this.teamMapper = teamMapper;
@ -61,7 +61,8 @@ public class TournamentEndpoint {
@Secured("ROLE_USER")
@ResponseStatus(HttpStatus.OK)
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Get a list of all tournaments from a specific Organizer", security = @SecurityRequirement(name = "apiKey"))
@Operation(summary = "Get a list of all tournaments from a specific Organizer",
security = @SecurityRequirement(name = "apiKey"))
public ResponseEntity<List<TournamentListDto>> tournaments(Authentication authentication) {
LOG.info("GET {}", BASE_ENDPOINT);
var tournaments = tournamentService.findAllByOrganizer(authentication.getName());
@ -73,11 +74,11 @@ public class TournamentEndpoint {
@PostMapping
@Operation(summary = "Create a new tournament", security = @SecurityRequirement(name = "apiKey"))
public TournamentDto createTournament(@Valid @RequestBody CreateTournamentDto messageDto,
Authentication authentication) {
Authentication authentication) {
LOG.info("POST /api/v1/tournaments body: {}", messageDto);
return tournamentMapper.entityToDto(tournamentService.create(tournamentMapper.createDtoToEntity(messageDto),
authentication.getName()));
authentication.getName()));
}
@Secured("ROLE_USER")
@ -85,29 +86,13 @@ public class TournamentEndpoint {
@PostMapping(value = "{id}/qualification-matches")
@Operation(summary = "Create a new tournament", security = @SecurityRequirement(name = "apiKey"))
public List<TournamentQualificationMatchDto> generateQualificationMatches(
@PathVariable(name = "id") Long tournamentId, Authentication authentication) {
@PathVariable(name = "id") Long tournamentId, Authentication authentication) {
LOG.info("POST /api/v1/tournaments/{}/generate-qualification-matches", tournamentId);
return tournamentService.generateQualificationMatchesForTournament(tournamentId, authentication.getName())
.stream()
.map(tournamentMapper::qualificationMatchEntityToDto)
.toList();
}
@Secured("ROLE_USER")
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "{id}/teams")
@Operation(summary = "Get teams for a tournament", security = @SecurityRequirement(name = "apiKey"))
public List<TeamDto> getTournamentTeams(@PathVariable(name = "id") Long tournamentId,
Authentication authentication) {
LOG.info("GET /api/v1/tournaments/{}/teams", tournamentId);
// check if user is organizer of tournament
if (!tournamentService.isOrganizer(authentication.getName(), tournamentId)) {
throw new AccessDeniedException("Current user isn't organizer of tournament.");
}
return tournamentService.getTournamentTeams(tournamentId).stream().map(teamMapper::entityToDto).toList();
.stream()
.map(tournamentMapper::qualificationMatchEntityToDto)
.toList();
}
@Secured("ROLE_USER")
@ -115,7 +100,7 @@ public class TournamentEndpoint {
@DeleteMapping("/{tournamentId}")
@Operation(summary = "Delete a tournament", security = @SecurityRequirement(name = "apiKey"))
public ResponseEntity<Void> deleteTournament(@PathVariable("tournamentId") long tournamentId,
Authentication authentication) {
Authentication authentication) {
LOG.info("DELETE {}/{}", BASE_ENDPOINT, tournamentId);
tournamentService.deleteTournament(tournamentId, authentication.getName());
return ResponseEntity.noContent().build();
@ -126,13 +111,13 @@ public class TournamentEndpoint {
@PostMapping(value = "{id}/generate-ko-matches")
@Operation(summary = "Create a new tournament", security = @SecurityRequirement(name = "apiKey"))
public void generateKoMatches(
@PathVariable(name = "id") Long tournamentId,
Authentication authentication) {
@PathVariable(name = "id") Long tournamentId,
Authentication authentication) {
LOG.info("POST /api/v1/tournaments/{}/generate-ko-matches", tournamentId);
tournamentService.generateKoMatchesForTournament(
tournamentId,
authentication.getName());
tournamentId,
authentication.getName());
}
@ -157,13 +142,47 @@ public class TournamentEndpoint {
public record SignupTeamResponseDto(SignupTeamResult signupTeamResult) {
}
@Secured("ROLE_USER")
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "{id}/teams")
@Operation(summary = "Get teams for a tournament", security = @SecurityRequirement(name = "apiKey"))
public List<TeamDto> getTournamentTeams(@PathVariable(name = "id") Long tournamentId,
Authentication authentication) {
LOG.info("GET /api/v1/tournaments/{}/teams", tournamentId);
// check if user is organizer of tournament
if (!tournamentService.isOrganizer(authentication.getName(), tournamentId)) {
throw new AccessDeniedException("Current user isn't organizer of tournament.");
}
return tournamentService.getTournamentTeams(tournamentId).stream().map(teamMapper::entityToDto).toList();
}
@Secured("ROLE_USER")
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping(value = "{tournamentId}/teams/{teamId}")
@Operation(summary = "Delete team from a tournament", security = @SecurityRequirement(name = "apiKey"))
public ResponseEntity<Void> deleteTournamentTeam(@PathVariable("tournamentId") Long tournamentId,
@PathVariable("teamId") Long teamId,
Authentication authentication
) {
LOG.info("Delete /api/v1/tournaments/{}/teams/{}", tournamentId, teamId);
// check if user is organizer of tournament
if (!tournamentService.isOrganizer(authentication.getName(), tournamentId)) {
throw new AccessDeniedException("Current user isn't organizer of tournament.");
}
tournamentService.deleteTeam(tournamentId, teamId);
return ResponseEntity.noContent().build();
}
@PermitAll
@ResponseStatus(HttpStatus.OK) // No location header, thus 200
@PostMapping("{tournamentId}/teams")
@Operation(summary = "Create a new team")
public ResponseEntity<SignupTeamResponseDto> signupTeamForTournament(
@PathVariable("tournamentId") long tournamentId,
@Valid @RequestBody CreateTeamDto messageDto) {
@PathVariable("tournamentId") long tournamentId,
@Valid @RequestBody CreateTeamDto messageDto) {
LOG.info("POST {} body: {}", BASE_ENDPOINT, messageDto);
final var signupResult = tournamentService.signupTeamForTournament(tournamentId, messageDto.name);
if (signupResult != SignupTeamResult.SUCCESS) {

View file

@ -3,6 +3,7 @@ package at.ac.tuwien.sepr.groupphase.backend.endpoint.exceptionhandler;
import at.ac.tuwien.sepr.groupphase.backend.exception.NotFoundException;
import at.ac.tuwien.sepr.groupphase.backend.exception.PreconditionFailedException;
import at.ac.tuwien.sepr.groupphase.backend.exception.TournamentAlreadyStartedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
@ -37,7 +38,7 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
/**
* Use the @ExceptionHandler annotation to write handler for custom exceptions.
*/
@ExceptionHandler(value = { NotFoundException.class })
@ExceptionHandler(value = {NotFoundException.class})
protected ResponseEntity<Object> handleNotFound(RuntimeException ex, WebRequest request) {
LOGGER.warn(ex.getMessage());
return handleExceptionInternal(ex, ex.getMessage(), new HttpHeaders(), HttpStatus.NOT_FOUND, request);
@ -49,15 +50,15 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
*/
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
// Get all errors
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(err -> err.getField() + " " + err.getDefaultMessage())
.collect(Collectors.toList());
.getFieldErrors()
.stream()
.map(err -> err.getField() + " " + err.getDefaultMessage())
.collect(Collectors.toList());
body.put("Validation errors", errors);
return new ResponseEntity<>(body.toString(), headers, status);
@ -67,18 +68,18 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
* Overrides the handling of PreconditionFailedException that sends a HTTP 400
* response.
*/
@ExceptionHandler(value = { PreconditionFailedException.class })
@ExceptionHandler(value = {PreconditionFailedException.class})
protected ResponseEntity<Object> handleAccessPreconditionFailedException(RuntimeException ex, WebRequest request) {
var preconditionException = (PreconditionFailedException) ex;
return handleExceptionInternal(
ex,
String.format("A precondition wasn't met: %s", preconditionException.getMessage()),
new HttpHeaders(),
HttpStatus.BAD_REQUEST,
request);
ex,
String.format("A precondition wasn't met: %s", preconditionException.getMessage()),
new HttpHeaders(),
HttpStatus.BAD_REQUEST,
request);
}
@ExceptionHandler({ BadCredentialsException.class, UsernameNotFoundException.class })
@ExceptionHandler({BadCredentialsException.class, UsernameNotFoundException.class})
protected ResponseEntity<Object> handleBadCredentialsException(RuntimeException ex, WebRequest request) {
LOGGER.debug(ex.getMessage());
@ -90,4 +91,16 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
HttpStatus.FORBIDDEN,
request);
}
@ExceptionHandler({TournamentAlreadyStartedException.class})
protected ResponseEntity<Object> handleTournamentAlreadyStartedException(RuntimeException ex, WebRequest request) {
LOGGER.debug(ex.getMessage());
return handleExceptionInternal(
ex,
"Tournament already started",
new HttpHeaders(),
HttpStatus.CONFLICT,
request);
}
}

View file

@ -0,0 +1,19 @@
package at.ac.tuwien.sepr.groupphase.backend.exception;
public class TournamentAlreadyStartedException extends RuntimeException {
public TournamentAlreadyStartedException() {
}
public TournamentAlreadyStartedException(String message) {
super(message);
}
public TournamentAlreadyStartedException(String message, Throwable cause) {
super(message, cause);
}
public TournamentAlreadyStartedException(Exception e) {
super(e);
}
}

View file

@ -94,4 +94,12 @@ public interface TournamentService {
* (domain-level authorization)
*/
void generateKoMatchesForTournament(Long tournamentId, String subject);
/**
* Delete a team from a tournament.
*
* @param tournamentId the id of the tournament entity
* @param teamId the id of the team entity
*/
void deleteTeam(Long tournamentId, Long teamId);
}

View file

@ -8,6 +8,7 @@ import java.util.List;
import java.util.stream.Collectors;
import at.ac.tuwien.sepr.groupphase.backend.entity.Team;
import at.ac.tuwien.sepr.groupphase.backend.exception.TournamentAlreadyStartedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
@ -42,12 +43,12 @@ public class TournamentServiceImpl implements TournamentService {
private KoStandingsRepository koStandingsRepository;
public TournamentServiceImpl(
TournamentRepository tournamentRepository,
TeamRepository teamRepository,
QualificationMatchRepository qualificatonRepository,
UserRepository userRepository,
QualificationParticipationRepository qualificationParticipationRepository,
KoStandingsRepository koStandingsRepository) {
TournamentRepository tournamentRepository,
TeamRepository teamRepository,
QualificationMatchRepository qualificatonRepository,
UserRepository userRepository,
QualificationParticipationRepository qualificationParticipationRepository,
KoStandingsRepository koStandingsRepository) {
super();
this.teamRepository = teamRepository;
@ -84,33 +85,33 @@ public class TournamentServiceImpl implements TournamentService {
@Override
public List<QualificationMatch> generateQualificationMatchesForTournament(Long tournamentId, String currentUserName)
throws PreconditionFailedException, AccessDeniedException, NotFoundException {
throws PreconditionFailedException, AccessDeniedException, NotFoundException {
LOGGER.debug("Create qualifying matches for tournament with id {}", tournamentId);
var tournamentOrganizer = tournamentRepository
.findById(tournamentId)
.orElseThrow(() -> new NotFoundException())
.getOrganizer();
.findById(tournamentId)
.orElseThrow(() -> new NotFoundException())
.getOrganizer();
if (tournamentOrganizer == null || !tournamentOrganizer.getUsername().equals(currentUserName)) {
LOGGER.debug(
"Couldn't create qualifying matches for tournament with id {}, because the user who started the process isn't the same as the creator of the tournament.",
tournamentId);
"Couldn't create qualifying matches for tournament with id {}, because the user who started the process isn't the same as the creator of the tournament.",
tournamentId);
throw new AccessDeniedException("Current user isn't organizer of tournament.");
}
var teams = teamRepository.findAllByTournamentId(tournamentId);
if (teams.size() < 16) {
LOGGER.debug(
"Couldn't create qualifying matches for tournament with id {}, because there were less than 16 teams assigned to the tournament.",
tournamentId);
"Couldn't create qualifying matches for tournament with id {}, because there were less than 16 teams assigned to the tournament.",
tournamentId);
throw new PreconditionFailedException("Not enough teams in specified tournament.");
}
if (teams.stream().anyMatch(t -> qualificationParticipationRepository.existsByTeamId(t.getId()))) {
LOGGER.debug(
"Couldn't create qualifying matches for tournament with id {}, because there were already qualification matches assigned.",
tournamentId);
"Couldn't create qualifying matches for tournament with id {}, because there were already qualification matches assigned.",
tournamentId);
throw new PreconditionFailedException("Qualification matches already created for tournament.");
}
@ -146,7 +147,7 @@ public class TournamentServiceImpl implements TournamentService {
LOGGER.debug("Create new team {} for tournament {}", name, tournamentId);
final var tournament = tournamentRepository.findById(tournamentId)
.orElseThrow(() -> new IllegalArgumentException("Tournament not found"));
.orElseThrow(() -> new IllegalArgumentException("Tournament not found"));
final var newTeam = new Team(name, tournament);
var result = tournament.signupTeam(newTeam);
@ -163,7 +164,7 @@ public class TournamentServiceImpl implements TournamentService {
LOGGER.debug("Get basic information about tournament {}", tournamentId);
var tournament = tournamentRepository.findById(tournamentId)
.orElseThrow(() -> new IllegalArgumentException("Tournament not found"));
.orElseThrow(() -> new IllegalArgumentException("Tournament not found"));
return tournament;
}
@ -183,11 +184,11 @@ public class TournamentServiceImpl implements TournamentService {
@Transactional
public void deleteTournament(long tournamentId, String currentUserName)
throws NotFoundException, AccessDeniedException {
throws NotFoundException, AccessDeniedException {
LOGGER.debug("Deleting tournament with id {}", tournamentId);
Tournament tournament = tournamentRepository.findById(tournamentId)
.orElseThrow(() -> new NotFoundException("Tournament not found"));
.orElseThrow(() -> new NotFoundException("Tournament not found"));
if (!tournament.getOrganizer().getUsername().equals(currentUserName)) {
throw new AccessDeniedException("You do not have permission to delete this tournament");
@ -199,15 +200,15 @@ public class TournamentServiceImpl implements TournamentService {
@Transactional
public void generateKoMatchesForTournament(Long tournamentId, String subjectName)
throws NotFoundException, AccessDeniedException {
throws NotFoundException, AccessDeniedException {
LOGGER.debug("Create knockout matches for tournament with id {}", tournamentId);
// authorization
final Tournament tournament = tournamentRepository.getReferenceById(tournamentId);
if (!tournament.getOrganizer().getUsername().equals(subjectName)) {
LOGGER.debug(
"Subject {} illegally tried to generate KO phase of non-owned tournament {}",
subjectName, tournamentId);
"Subject {} illegally tried to generate KO phase of non-owned tournament {}",
subjectName, tournamentId);
throw new AccessDeniedException("Current user isn't organizer of tournament.");
}
@ -216,8 +217,8 @@ public class TournamentServiceImpl implements TournamentService {
qualiIncomplete = matches.stream().anyMatch(m -> m.getWinner() == null);
if (qualiIncomplete) {
LOGGER.debug(
"Couldn't create knockout matches for tournament with id {}, because there were still qualification matches running.",
tournamentId);
"Couldn't create knockout matches for tournament with id {}, because there were still qualification matches running.",
tournamentId);
throw new PreconditionFailedException("Qualification matches still running.");
}
@ -230,14 +231,14 @@ public class TournamentServiceImpl implements TournamentService {
final var participations = qualificationParticipationRepository.findByQualificationMatchIn(matches);
final var pointsByTeam = participations.stream()
.collect(Collectors.groupingBy(QualificationParticipation::getTeam,
Collectors.summingLong(p -> p.getQualificiatonMatch().getWinnerPoints())));
.collect(Collectors.groupingBy(QualificationParticipation::getTeam,
Collectors.summingLong(p -> p.getQualificiatonMatch().getWinnerPoints())));
final var rankedTeamIds = pointsByTeam.entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue(), a.getValue())).map(e -> e.getKey())
.limit(limit)
.map(team -> new KoStanding(tournament, null, team))
.toArray(KoStanding[]::new);
.sorted((a, b) -> Long.compare(b.getValue(), a.getValue())).map(e -> e.getKey())
.limit(limit)
.map(team -> new KoStanding(tournament, null, team))
.toArray(KoStanding[]::new);
// map the teams to the leaf layer of the KO tree
// undefined ranking logic, so i just cross-match the teams like in wendy's
@ -258,7 +259,7 @@ public class TournamentServiceImpl implements TournamentService {
for (int i = 0; i < layer.length; i++) {
final var preceeding = List.of(prevLayer[i * 2], prevLayer[i * 2 + 1]);
layer[i] = new KoStanding(tournament,
preceeding, null);
preceeding, null);
// passing the preceeding standings to the parent does not help us, as the child
// owns the relationship
for (var p : preceeding) {
@ -277,4 +278,21 @@ public class TournamentServiceImpl implements TournamentService {
koStandingsRepository.saveAndFlush(layer[0]);
}
@Override
public void deleteTeam(Long tournamentId, Long teamId)
throws NotFoundException, IllegalArgumentException, IllegalStateException {
LOGGER.debug("Delete team with id {} from tournament with id {}", teamId, tournamentId);
var team = teamRepository.findById(teamId).orElseThrow(() -> new NotFoundException("Team not found in tournament"));
var tournament = team.getTournament();
if (!tournament.getId().equals(tournamentId)) {
throw new NotFoundException("Team not found in tournament");
}
if ((long) tournament.getQualificationMatches().size() > 0) {
throw new TournamentAlreadyStartedException();
}
teamRepository.delete(team);
}
}