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

feat/#16/team-self-registration

This commit is contained in:
Matthias Hofmarcher 2024-05-19 22:06:11 +00:00
parent 13dcb3e951
commit 7e2a3cb942
27 changed files with 667 additions and 50 deletions

View file

@ -8,6 +8,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.Authentication;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -20,14 +22,17 @@ import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentListDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentQualificationMatchDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.mapper.TournamentMapper;
import at.ac.tuwien.sepr.groupphase.backend.entity.Tournament.SignupTeamResult;
import at.ac.tuwien.sepr.groupphase.backend.service.TournamentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@RestController
@RequestMapping(value = TournamentEndpoint.BASE_ENDPOINT)
@ -59,14 +64,14 @@ public class TournamentEndpoint {
@PostMapping
@Operation(summary = "Create a new tournament", security = @SecurityRequirement(name = "apiKey"))
public TournamentDto createTournament(
@Valid @RequestBody CreateTournamentDto messageDto,
Authentication authentication) {
@Valid @RequestBody CreateTournamentDto messageDto,
Authentication authentication) {
LOG.info("POST /api/v1/tournaments body: {}", messageDto);
return tournamentMapper.entityToDto(
tournamentService.create(
tournamentMapper.createDtoToEntity(messageDto),
authentication.getName()));
tournamentService.create(
tournamentMapper.createDtoToEntity(messageDto),
authentication.getName()));
}
@Secured("ROLE_USER")
@ -74,15 +79,52 @@ public class TournamentEndpoint {
@PostMapping(value = "{id}/generate-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();
.stream()
.map(tournamentMapper::qualificationMatchEntityToDto)
.toList();
}
// as of now, information is accessible to everyone
@PermitAll
@GetMapping("{tournamentId}/public")
@Operation(summary = "Get public info about tournament")
public ResponseEntity<TournamentDto> get(@PathVariable("tournamentId") long tournamentId) {
LOG.info("GET {}{}", BASE_ENDPOINT, tournamentId);
final var tournament = tournamentService.findOne(tournamentId);
final var dto = TournamentDto.fromEntity(tournament);
return ResponseEntity.ok(dto);
}
// region Team
record CreateTeamDto(@NotBlank @Size(min = 3, max = 20) String name) {
}
/**
* Response for team signup. Compared to enum, has openapi-generator support.
*/
public record SignupTeamResponseDto(SignupTeamResult signupTeamResult) {
}
@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) {
LOG.info("POST {} body: {}", BASE_ENDPOINT, messageDto);
final var signupResult = tournamentService.signupTeamForTournament(tournamentId, messageDto.name);
if (signupResult != SignupTeamResult.SUCCESS) {
return ResponseEntity.badRequest().body(new SignupTeamResponseDto(signupResult));
}
return ResponseEntity.ok(new SignupTeamResponseDto(SignupTeamResult.SUCCESS));
}
// endregion team
}

View file

@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
import at.ac.tuwien.sepr.groupphase.backend.entity.Tournament;
public record TournamentDto(
Long id,
String name,
@ -11,4 +13,13 @@ public record TournamentDto(
LocalDateTime registrationEnd,
Long maxParticipants,
String description) {
public static TournamentDto fromEntity(Tournament e) {
return new TournamentDto(
e.getId(),
e.getName(),
e.getRegistrationEnd(),
e.getMaxParticipants(),
e.getDescription());
}
}

View file

@ -7,6 +7,7 @@ import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.validation.constraints.Size;
import java.util.List;
@ -15,10 +16,12 @@ public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Boolean checkedIn;
@ManyToOne
@Size(min = 3, max = 20)
private String name;
private boolean checkedIn;
@ManyToOne(optional = false)
private Tournament tournament;
@OneToMany(mappedBy = "team", cascade = CascadeType.REMOVE)

View file

@ -1,23 +1,25 @@
package at.ac.tuwien.sepr.groupphase.backend.entity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import org.hibernate.Hibernate;
@Entity
public class Tournament {
@Id
@GeneratedValue
private Long id;
public Tournament(String name, LocalDateTime registrationEnd, Long maxParticipants, String description, ApplicationUser organizer, List<Team> teams, List<BeerPongTable> tables) {
public Tournament(String name, LocalDateTime registrationEnd, Long maxParticipants, String description,
ApplicationUser organizer, List<Team> teams, List<BeerPongTable> tables) {
this.name = name;
this.registrationEnd = registrationEnd;
this.maxParticipants = maxParticipants;
@ -31,6 +33,7 @@ public class Tournament {
private LocalDateTime registrationEnd;
private Long maxParticipants;
private String description;
@ManyToOne
private ApplicationUser organizer;
@ -64,6 +67,7 @@ public class Tournament {
return maxParticipants;
}
// requires careful validation, disabled for now
public void setMaxParticipants(Long maxParticipants) {
this.maxParticipants = maxParticipants;
}
@ -80,6 +84,7 @@ public class Tournament {
return organizer;
}
// hibernate-required
public void setOrganizer(ApplicationUser organizer) {
this.organizer = organizer;
}
@ -91,4 +96,52 @@ public class Tournament {
public void setRegistrationEnd(LocalDateTime registrationEnd) {
this.registrationEnd = registrationEnd;
}
public List<Team> getTeams() {
return Collections.unmodifiableList(teams);
}
public enum SignupTeamResult {
SUCCESS,
REGISTRATION_CLOSED,
MAX_PARTICIPANTS_REACHED,
TEAM_ALREADY_EXISTS
}
/**
* Precondition: transaction that manages this tournament is still active for
* the lazy initialization to work.
*
* @return Result enum
*/
public SignupTeamResult signupTeam(Team team) {
Hibernate.initialize(teams);
if (registrationEnd.isBefore(LocalDateTime.now())) {
return SignupTeamResult.REGISTRATION_CLOSED;
}
if (teams.size() >= maxParticipants) {
return SignupTeamResult.MAX_PARTICIPANTS_REACHED;
}
if (teams.stream().anyMatch(
t -> t.getName().equals(team.getName()))) {
return SignupTeamResult.TEAM_ALREADY_EXISTS;
}
// relation is not set otherwise
team.setTournament(this);
teams.add(team);
return SignupTeamResult.SUCCESS;
}
// hibernate-required
private void setTeams(List<Team> teams) {
this.teams = teams;
}
public List<BeerPongTable> getTables() {
return tables;
}
public void setTables(List<BeerPongTable> tables) {
this.tables = tables;
}
}

View file

@ -8,6 +8,7 @@ import at.ac.tuwien.sepr.groupphase.backend.entity.QualificationMatch;
import at.ac.tuwien.sepr.groupphase.backend.entity.Tournament;
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.entity.Tournament.SignupTeamResult;
public interface TournamentService {
/**
@ -40,4 +41,20 @@ public interface TournamentService {
*/
List<QualificationMatch> generateQualificationMatchesForTournament(Long tournamentId, String currentUserName)
throws PreconditionFailedException, AccessDeniedException, NotFoundException;
/**
* Create a new tournament.
* name may not be duplicate within the same tournament
*
* @return created tournament entity
*/
SignupTeamResult signupTeamForTournament(long tournamentId, String name);
/**
* Find a single tournament entity by id.
*
* @param tournamentId the id of the tournament entity
* @return the tournament entity
*/
Tournament findOne(long tournamentId);
}

View file

@ -17,9 +17,12 @@ 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.repository.QualificationMatchRepository;
import at.ac.tuwien.sepr.groupphase.backend.repository.TeamRepository;
import at.ac.tuwien.sepr.groupphase.backend.entity.Team;
import at.ac.tuwien.sepr.groupphase.backend.entity.Tournament.SignupTeamResult;
import at.ac.tuwien.sepr.groupphase.backend.repository.TournamentRepository;
import at.ac.tuwien.sepr.groupphase.backend.repository.UserRepository;
import at.ac.tuwien.sepr.groupphase.backend.service.TournamentService;
import jakarta.transaction.Transactional;
@Service
public class TournamentServiceImpl implements TournamentService {
@ -56,6 +59,7 @@ public class TournamentServiceImpl implements TournamentService {
return tournamentRepository.findAllByOrganizerIdOrderByNameAsc(organizerId);
}
@Override
public Tournament create(Tournament tournament, String currentUserName) {
LOGGER.debug("Create new tournament {}", tournament);
@ -120,4 +124,34 @@ public class TournamentServiceImpl implements TournamentService {
return matches;
}
@Override
@Transactional
public SignupTeamResult signupTeamForTournament(long tournamentId, String name) {
LOGGER.debug("Create new team {} for tournament {}", name, tournamentId);
final var tournament = tournamentRepository.findById(tournamentId)
.orElseThrow(() -> new IllegalArgumentException("Tournament not found"));
final var newTeam = new Team();
newTeam.setName(name);
var result = tournament.signupTeam(newTeam);
if (result != SignupTeamResult.SUCCESS) {
return result;
}
tournamentRepository.saveAndFlush(tournament);
return SignupTeamResult.SUCCESS;
}
@Override
public Tournament findOne(long tournamentId) {
LOGGER.debug("Get basic information about tournament {}", tournamentId);
var tournament = tournamentRepository.findById(tournamentId)
.orElseThrow(() -> new IllegalArgumentException("Tournament not found"));
return tournament;
}
}

View file

@ -21,6 +21,9 @@ spring:
# is disabled since it breaks the principle of least astonishment and leads to bad performance. To learn more,
# follow this link: https://bit.ly/2LaX9ku
open-in-view: false
h2:
console:
enabled: true
security:
auth:
@ -65,7 +68,6 @@ management:
springdoc:
default-consumes-media-type: application/json
default-produces-media-type: application/json
---
spring:
config:

View file

@ -1,12 +1,14 @@
package at.ac.tuwien.sepr.groupphase.backend.integrationtest;
import at.ac.tuwien.sepr.groupphase.backend.basetest.TestData;
import at.ac.tuwien.sepr.groupphase.backend.config.properties.SecurityProperties;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.CreateTournamentDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentDto;
import at.ac.tuwien.sepr.groupphase.backend.repository.MessageRepository;
import at.ac.tuwien.sepr.groupphase.backend.security.JwtTokenizer;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -20,12 +22,14 @@ import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import at.ac.tuwien.sepr.groupphase.backend.basetest.TestData;
import at.ac.tuwien.sepr.groupphase.backend.config.properties.SecurityProperties;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.CreateTournamentDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentDto;
import at.ac.tuwien.sepr.groupphase.backend.repository.MessageRepository;
import at.ac.tuwien.sepr.groupphase.backend.security.JwtTokenizer;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

View file

@ -38,7 +38,7 @@ public class TournamentServiceTest implements TestData {
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
tournament.setMaxParticipants(64L);
tournament = tournamentService.create(tournament, user.getUsername());
assertNotNull(tournament);

View file

@ -0,0 +1,37 @@
package at.ac.tuwien.sepr.groupphase.backend.unittests;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDateTime;
import java.util.LinkedList;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import at.ac.tuwien.sepr.groupphase.backend.basetest.TestData;
import at.ac.tuwien.sepr.groupphase.backend.entity.Team;
import at.ac.tuwien.sepr.groupphase.backend.entity.Tournament;
import at.ac.tuwien.sepr.groupphase.backend.entity.Tournament.SignupTeamResult;
@ActiveProfiles("test")
public class TournamentTest implements TestData {
@Test
public void signUpTeam_givenDuplicateName_Fails() {
final var tournament = new Tournament(
"TestTournament",
LocalDateTime.MIN,
64L,
"TestDescription",
null,
new LinkedList<>(),
null
);
tournament.setRegistrationEnd(LocalDateTime.MAX);
var team1 = new Team();
team1.setName("DuplicateName");
assertEquals(SignupTeamResult.SUCCESS, tournament.signupTeam(team1));
var team2 = new Team();
team2.setName("DuplicateName");
assertEquals(SignupTeamResult.TEAM_ALREADY_EXISTS, tournament.signupTeam(team2));
}
}

View file

@ -1 +1 @@
openapi-generated/**/* -merge -diff
openapi-generated/**/* -merge -diff

17
frontend/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200",
//Prevent Chrome from debugging library code
"skipFiles": ["node_modules/**/*.js", "lib/**/*.js"]
}
]
}

24
frontend/.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,24 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View file

@ -11,10 +11,12 @@ configuration.ts
encoder.ts
git_push.sh
index.ts
model/createTeamDto.ts
model/createTournamentDto.ts
model/detailedMessageDto.ts
model/messageInquiryDto.ts
model/models.ts
model/signupTeamResponseDto.ts
model/simpleMessageDto.ts
model/tournamentDto.ts
model/tournamentListDto.ts

View file

@ -18,9 +18,13 @@ import { HttpClient, HttpHeaders, HttpParams,
import { CustomHttpParameterCodec } from '../encoder';
import { Observable } from 'rxjs';
// @ts-ignore
import { CreateTeamDto } from '../model/createTeamDto';
// @ts-ignore
import { CreateTournamentDto } from '../model/createTournamentDto';
// @ts-ignore
import { SignupTeamResponseDto } from '../model/signupTeamResponseDto';
// @ts-ignore
import { TournamentDto } from '../model/tournamentDto';
// @ts-ignore
import { TournamentListDto } from '../model/tournamentListDto';
@ -235,6 +239,148 @@ export class TournamentEndpointService {
);
}
/**
* Get public info about tournament
* @param tournamentId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public get(tournamentId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<TournamentDto>;
public get(tournamentId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<TournamentDto>>;
public get(tournamentId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<TournamentDto>>;
public get(tournamentId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {
if (tournamentId === null || tournamentId === undefined) {
throw new Error('Required parameter tournamentId was null or undefined when calling get.');
}
let localVarHeaders = this.defaultHeaders;
let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
let localVarHttpContext: HttpContext | undefined = options && options.context;
if (localVarHttpContext === undefined) {
localVarHttpContext = new HttpContext();
}
let localVarTransferCache: boolean | undefined = options && options.transferCache;
if (localVarTransferCache === undefined) {
localVarTransferCache = true;
}
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
responseType_ = 'json';
} else {
responseType_ = 'blob';
}
}
let localVarPath = `/api/v1/tournaments/${this.configuration.encodeParam({name: "tournamentId", value: tournamentId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/public`;
return this.httpClient.request<TournamentDto>('get', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
transferCache: localVarTransferCache,
reportProgress: reportProgress
}
);
}
/**
* Create a new team
* @param tournamentId
* @param createTeamDto
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public signupTeamForTournament(tournamentId: number, createTeamDto: CreateTeamDto, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<SignupTeamResponseDto>;
public signupTeamForTournament(tournamentId: number, createTeamDto: CreateTeamDto, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<SignupTeamResponseDto>>;
public signupTeamForTournament(tournamentId: number, createTeamDto: CreateTeamDto, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<SignupTeamResponseDto>>;
public signupTeamForTournament(tournamentId: number, createTeamDto: CreateTeamDto, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {
if (tournamentId === null || tournamentId === undefined) {
throw new Error('Required parameter tournamentId was null or undefined when calling signupTeamForTournament.');
}
if (createTeamDto === null || createTeamDto === undefined) {
throw new Error('Required parameter createTeamDto was null or undefined when calling signupTeamForTournament.');
}
let localVarHeaders = this.defaultHeaders;
let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
let localVarHttpContext: HttpContext | undefined = options && options.context;
if (localVarHttpContext === undefined) {
localVarHttpContext = new HttpContext();
}
let localVarTransferCache: boolean | undefined = options && options.transferCache;
if (localVarTransferCache === undefined) {
localVarTransferCache = true;
}
// to determine the Content-Type header
const consumes: string[] = [
'application/json'
];
const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected);
}
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
responseType_ = 'json';
} else {
responseType_ = 'blob';
}
}
let localVarPath = `/api/v1/tournaments/${this.configuration.encodeParam({name: "tournamentId", value: tournamentId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/teams`;
return this.httpClient.request<SignupTeamResponseDto>('post', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
body: createTeamDto,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
transferCache: localVarTransferCache,
reportProgress: reportProgress
}
);
}
/**
* Get a list of all tournaments from a specific Organizer
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.

View file

@ -94,6 +94,7 @@ export class UserEndpointService {
}
/**
* Delete user and all data belonging to them(Tournaments, Teams, etc) from the database.
* @param username
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
@ -157,6 +158,7 @@ export class UserEndpointService {
}
/**
* Get detailed information about user and their tournaments.
* @param username
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.

View file

@ -0,0 +1,17 @@
/**
* OpenAPI definition
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: v0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface CreateTeamDto {
name: string;
}

View file

@ -1,6 +1,8 @@
export * from './createTeamDto';
export * from './createTournamentDto';
export * from './detailedMessageDto';
export * from './messageInquiryDto';
export * from './signupTeamResponseDto';
export * from './simpleMessageDto';
export * from './tournamentDto';
export * from './tournamentListDto';

View file

@ -0,0 +1,18 @@
/**
* OpenAPI definition
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: v0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface SignupTeamResponse {
registrationClosed?: boolean;
teamAlreadyExists?: boolean;
}

View file

@ -0,0 +1,27 @@
/**
* OpenAPI definition
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: v0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface SignupTeamResponseDto {
signupTeamResult?: SignupTeamResponseDto.SignupTeamResultEnum;
}
export namespace SignupTeamResponseDto {
export type SignupTeamResultEnum = 'SUCCESS' | 'REGISTRATION_CLOSED' | 'MAX_PARTICIPANTS_REACHED' | 'TEAM_ALREADY_EXISTS';
export const SignupTeamResultEnum = {
Success: 'SUCCESS' as SignupTeamResultEnum,
RegistrationClosed: 'REGISTRATION_CLOSED' as SignupTeamResultEnum,
MaxParticipantsReached: 'MAX_PARTICIPANTS_REACHED' as SignupTeamResultEnum,
TeamAlreadyExists: 'TEAM_ALREADY_EXISTS' as SignupTeamResultEnum
};
}

View file

@ -8,27 +8,42 @@ import { UserRegisterComponent } from './components/user-register/user-register.
import { TournamentsComponent } from './components/tournaments/tournaments.component';
import { TournamentCreateComponent } from './components/tournament-create/tournament-create.component';
import { UserDetailComponent } from './components/user-detail/user-detail.component';
import { TeamSignupComponent } from './components/tournament/team-signup/team-signup.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginDialogComponent },
{ path: 'register', component: UserRegisterComponent },
{ path: 'message', canActivate: mapToCanActivate([AuthGuard]), component: MessageComponent },
{
path: 'tournaments',
canActivate: mapToCanActivate([AuthGuard]),
component: TournamentsComponent,
},
{
path: 'tournaments/create',
canActivate: mapToCanActivate([AuthGuard]),
component: TournamentCreateComponent,
},
{
path: 'details/:username',
canActivate: mapToCanActivate([AuthGuard]),
component: UserDetailComponent,
},
{
path: 'tournaments',
children: [
{
path: 'list',
canActivate: mapToCanActivate([AuthGuard]),
component: TournamentsComponent,
},
{
path: 'create',
canActivate: mapToCanActivate([AuthGuard]),
component: TournamentCreateComponent,
},
{
path: ':tournamentId',
children: [
{
path: 'signup',
component: TeamSignupComponent,
},
],
},
],
},
];
@NgModule({

View file

@ -1,8 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { LOCALE_ID, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeaderComponent } from './components/header/header.component';
@ -14,13 +12,12 @@ import { MessageComponent } from './components/message/message.component';
import { httpInterceptorProviders } from './interceptors';
import { MatToolbar } from '@angular/material/toolbar';
import { MatIcon } from '@angular/material/icon';
import { MatButton, MatIconButton } from '@angular/material/button';
import { MatButton, MatButtonModule, MatIconButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
import { MatCardModule } from '@angular/material/card';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { TournamentsComponent } from './components/tournaments/tournaments.component';
import { TournamentCardComponent } from './components/tournament-card/tournament-card.component';
import { UserDetailComponent } from './components/user-detail/user-detail.component';
@ -30,6 +27,9 @@ import localeDe from '@angular/common/locales/de';
import { registerLocaleData } from '@angular/common';
registerLocaleData(localeDe);
import { ConfirmDialogComponent } from './components/confirm-dialog/confirm-dialog.component';
import { MatInputModule } from '@angular/material/input';
import { CommonModule } from '@angular/common';
import { TeamSignupComponent } from './components/tournament/team-signup/team-signup.component';
@NgModule({
declarations: [
@ -45,10 +45,10 @@ import { ConfirmDialogComponent } from './components/confirm-dialog/confirm-dial
UserDetailComponent,
UserEditDetailsForm,
UpdateUserComponent,
TeamSignupComponent,
ConfirmDialogComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
ReactiveFormsModule,
HttpClientModule,
@ -62,7 +62,9 @@ import { ConfirmDialogComponent } from './components/confirm-dialog/confirm-dial
BrowserAnimationsModule,
MatDialogModule,
MatFormFieldModule,
MatInput,
MatInputModule,
MatButtonModule,
CommonModule,
],
providers: [
httpInterceptorProviders,

View file

@ -105,7 +105,7 @@ export class TournamentCreateComponent {
this.snackBar.open('Successfully created new tournament', 'Close', {
duration: 3000,
});
this.router.navigate(['/tournaments']);
this.router.navigate(['/tournaments/list']);
},
error: async (er: HttpErrorResponse) => {
this.snackBar.open(

View file

@ -0,0 +1,32 @@
@if (isTournamentLoadedAndRegistrationOpen) {
@if (success) {
<h2>Signed up successfully</h2>
} @else {
<div class="form-container">
<form [formGroup]="newTeamNameForm" (ngSubmit)="createTeam()" class="form">
<h2>Sign Up for {{ tournament?.name }}</h2>
<mat-form-field appearance="outline">
<input matInput formControlName="name" placeholder="Team Name" autofocus />
@if (nameFormControl.errors?.required) {
<mat-error> A team name is required! </mat-error>
}
@if (nameFormControl.errors?.minlength) {
<mat-error>Team name must be at least 3 characters long!</mat-error>
}
@if (nameFormControl.errors?.maxlength) {
<mat-error>Team name must be at most 20 characters long!</mat-error>
}
@if (nameFormControl.errors?.teamAlreadyExists) {
<mat-error>Team name already exists!</mat-error>
}
@if (nameFormControl.errors?.maxParticipantsReached) {
<mat-error>No more spots left!</mat-error>
}
</mat-form-field>
<button mat-raised-button type="submit">Sign up!</button>
</form>
</div>
}
} @else {
<h2>Tournament not loaded or registration closed</h2>
}

View file

@ -0,0 +1,15 @@
.form-container {
display: flex;
justify-content: center;
}
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form {
display: flex;
flex-direction: column;
}

View file

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TeamSignupComponent } from './team-signup.component';
describe('TeamSignupComponent', () => {
let component: TeamSignupComponent;
let fixture: ComponentFixture<TeamSignupComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TeamSignupComponent],
}).compileComponents();
fixture = TestBed.createComponent(TeamSignupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,73 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TournamentEndpointService } from '../../../../../openapi-generated/api/tournamentEndpoint.service';
import { ActivatedRoute } from '@angular/router';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { SignupTeamResponseDto, TournamentDto } from 'openapi-generated';
import { catchError, tap, firstValueFrom, of } from 'rxjs';
import { extend } from 'lodash';
@Component({
selector: 'app-team-signup',
templateUrl: './team-signup.component.html',
styleUrl: './team-signup.component.scss',
})
export class TeamSignupComponent {
tournament: TournamentDto | undefined;
success = false;
nameFormControl = new FormControl('', [
Validators.minLength(3),
Validators.maxLength(20),
Validators.required,
]);
newTeamNameForm = new FormGroup({
name: this.nameFormControl,
});
get isTournamentLoadedAndRegistrationOpen(): boolean {
if (!this.tournament?.registrationEnd) {
return false;
}
return new Date() < new Date(this.tournament.registrationEnd);
}
constructor(
private tournamentService: TournamentEndpointService,
private route: ActivatedRoute,
) {
const tournamentId = Number(this.route.snapshot.paramMap.get('tournamentId'));
this.tournamentService.get(tournamentId).subscribe(tournament => {
this.tournament = tournament;
});
}
async createTeam() {
const request = this.tournamentService.signupTeamForTournament(
this.tournament?.id!,
{ name: this.newTeamNameForm.controls.name.value! },
'response',
);
request
.pipe(
catchError(e => {
if (e.status !== 400) {
return [];
}
const response = e.error as SignupTeamResponseDto;
switch (response.signupTeamResult) {
case SignupTeamResponseDto.SignupTeamResultEnum.TeamAlreadyExists:
this.nameFormControl.setErrors({ teamAlreadyExists: true });
return of();
case SignupTeamResponseDto.SignupTeamResultEnum.MaxParticipantsReached:
this.nameFormControl.setErrors({ maxParticipantsReached: true });
return of();
default:
return [];
}
this.nameFormControl.setErrors({ nameAlreadyTaken: true });
return of();
}),
)
.subscribe(response => (this.success = true));
}
}