1
0
Fork 0
mirror of https://codeberg.org/beerbrawl/beerbrawl.git synced 2024-09-23 05:40:51 +02:00

feat(#14): add button to copy tournament team self-registration link

This commit is contained in:
Christoph Heiss 2024-05-26 07:50:32 +00:00 committed by Matthias Hofmarcher
parent 413fee2936
commit 0e1f70aaf1
36 changed files with 528 additions and 261 deletions

View file

@ -62,10 +62,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>

View file

@ -2,15 +2,18 @@ package at.ac.tuwien.sepr.groupphase.backend.endpoint;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.CreateTeamDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.CreateTournamentDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.SignupTeamResponseDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TeamDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentCreateTeamDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentListDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentQualificationMatchDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentSignupTeamResponseDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.mapper.TeamMapper;
import at.ac.tuwien.sepr.groupphase.backend.exception.BadTournamentSignupTokenException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
@ -35,6 +38,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@ -133,8 +137,6 @@ public class TournamentEndpoint {
}
// region Team
@Secured("ROLE_USER")
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "{id}/teams")
@ -168,20 +170,36 @@ public class TournamentEndpoint {
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(
public ResponseEntity<TournamentSignupTeamResponseDto> 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));
@RequestParam("token") Optional<UUID> selfRegistrationToken,
@Valid @RequestBody TournamentCreateTeamDto createTeamDto
) {
LOG.info("POST {}/{}/teams?token={}", BASE_ENDPOINT, tournamentId, selfRegistrationToken);
LOG.debug("request body: {}", createTeamDto);
// Explicitly use an `Optional<>` and check it here, so we can return
// the appropriate error
// Otherwise, Spring Boot would just return a 400.
if (selfRegistrationToken.isEmpty()) {
throw new BadTournamentSignupTokenException();
}
return ResponseEntity.ok(new SignupTeamResponseDto(SignupTeamResult.SUCCESS));
final var signupResult = tournamentService.signupTeamForTournament(
tournamentId, selfRegistrationToken.get(), createTeamDto.name()
);
if (signupResult != SignupTeamResult.SUCCESS) {
return ResponseEntity.badRequest()
.body(new TournamentSignupTeamResponseDto(signupResult));
}
return ResponseEntity
.ok(new TournamentSignupTeamResponseDto(SignupTeamResult.SUCCESS));
}
// endregion team
}

View file

@ -3,5 +3,9 @@ package at.ac.tuwien.sepr.groupphase.backend.endpoint.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record CreateTeamDto(@NotBlank @Size(min = 3, max = 20) String name) {
}
public record TournamentCreateTeamDto(
@NotBlank
@Size(min = 3, max = 20)
String name
) {
}

View file

@ -3,11 +3,16 @@ package at.ac.tuwien.sepr.groupphase.backend.endpoint.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.UUID;
@Setter
@Getter
public class TournamentListDto {
@NotNull(message = "ID must not be null")
private Long id;
@ -25,51 +30,12 @@ public class TournamentListDto {
@Size(max = 1024)
private String description;
// Getters
public Long getId() {
return id;
}
public String getName() {
return name;
}
public LocalDateTime getRegistrationEnd() {
return registrationEnd;
}
public Long getMaxParticipants() {
return maxParticipants;
}
public String getDescription() {
return description;
}
// Setters
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setRegistrationEnd(LocalDateTime registrationEnd) {
this.registrationEnd = registrationEnd;
}
public void setMaxParticipants(Long maxParticipants) {
this.maxParticipants = maxParticipants;
}
public void setDescription(String description) {
this.description = description;
}
@NotNull
private UUID selfRegistrationToken;
@Override
public int hashCode() {
return Objects.hash(id, name, registrationEnd, maxParticipants, description);
return Objects.hash(id, name, registrationEnd, maxParticipants, description, selfRegistrationToken);
}
@Override
@ -85,7 +51,8 @@ public class TournamentListDto {
&& Objects.equals(name, that.name)
&& Objects.equals(registrationEnd, that.registrationEnd)
&& Objects.equals(maxParticipants, that.maxParticipants)
&& Objects.equals(description, that.description);
&& Objects.equals(description, that.description)
&& Objects.equals(selfRegistrationToken, that.selfRegistrationToken);
}
@Override
@ -102,6 +69,9 @@ public class TournamentListDto {
+ ", description='"
+ description
+ '\''
+ ", selfRegistrationToken='"
+ selfRegistrationToken
+ '\''
+ '}';
}
}

View file

@ -1,10 +1,11 @@
package at.ac.tuwien.sepr.groupphase.backend.endpoint.dto;
import at.ac.tuwien.sepr.groupphase.backend.entity.Tournament;
/**
* Response for team signup. Compared to enum, has openapi-generator support.
*/
public record SignupTeamResponseDto(Tournament.SignupTeamResult signupTeamResult) {
public record TournamentSignupTeamResponseDto(
Tournament.SignupTeamResult signupTeamResult
) {
}

View file

@ -1,5 +1,6 @@
package at.ac.tuwien.sepr.groupphase.backend.endpoint.exceptionhandler;
import at.ac.tuwien.sepr.groupphase.backend.exception.BadTournamentSignupTokenException;
import at.ac.tuwien.sepr.groupphase.backend.exception.NotFoundException;
import at.ac.tuwien.sepr.groupphase.backend.exception.PreconditionFailedException;
@ -103,4 +104,16 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
HttpStatus.CONFLICT,
request);
}
@ExceptionHandler({BadTournamentSignupTokenException.class})
protected ResponseEntity<Object> handleBadTournamentSignupTokenException(RuntimeException ex, WebRequest request) {
LOGGER.debug(ex.getMessage());
return handleExceptionInternal(
ex,
"self registration token missing or incorrect",
new HttpHeaders(),
HttpStatus.UNAUTHORIZED,
request);
}
}

View file

@ -8,6 +8,7 @@ import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.List;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentQualificationMatchDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentQualificationMatchParticipantDto;
import at.ac.tuwien.sepr.groupphase.backend.entity.QualificationMatch;

View file

@ -1,6 +1,7 @@
package at.ac.tuwien.sepr.groupphase.backend.entity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@ -14,6 +15,7 @@ import java.time.LocalDateTime;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
@ -21,24 +23,38 @@ import org.hibernate.annotations.OnDeleteAction;
/**
* The tournament entity.
* Owns the relation with teams for consistency reasons.
*
*
*/
@Entity
@Getter(value = AccessLevel.PUBLIC)
@Setter(value = AccessLevel.PRIVATE)
public class Tournament {
@Getter
@Id
@GeneratedValue
private Long id;
@Setter
@Getter
private String name;
@Setter
@Getter
private LocalDateTime registrationEnd;
@Getter
private Long maxParticipants;
@Setter
@Getter
private String description;
@Getter
@Column(nullable = false, updatable = false)
private UUID selfRegistrationToken;
@Setter
@Getter
@ManyToOne
private ApplicationUser organizer;
@OneToMany(mappedBy = Team_.TOURNAMENT, cascade = CascadeType.ALL, orphanRemoval = true)
@OnDelete(action = OnDeleteAction.CASCADE)
private List<Team> teams = new LinkedList<>();
@Getter
@OneToMany(mappedBy = BeerPongTable_.TOURNAMENT, cascade = CascadeType.ALL, orphanRemoval = true)
@OnDelete(action = OnDeleteAction.CASCADE)
private List<BeerPongTable> tables = new LinkedList<>();
@ -59,31 +75,18 @@ public class Tournament {
this.maxParticipants = maxParticipants;
this.description = description;
this.organizer = organizer;
this.selfRegistrationToken = UUID.randomUUID();
}
/**
* This object should be able to enforce its validity. It is not able to do that
* if it isn't even initialized
* if it isn't even initialized.
* Unfortunately required due to Spring Boot doing some things automagically in
* the background and needs this ...
*/
@Deprecated
public Tournament() {
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getMaxParticipants() {
return maxParticipants;
this.selfRegistrationToken = UUID.randomUUID();
}
// requires careful validation if updated retrospectively, please just use
@ -93,30 +96,6 @@ public class Tournament {
this.maxParticipants = maxParticipants;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public ApplicationUser getOrganizer() {
return organizer;
}
public void setOrganizer(ApplicationUser organizer) {
this.organizer = organizer;
}
public LocalDateTime getRegistrationEnd() {
return registrationEnd;
}
public void setRegistrationEnd(LocalDateTime registrationEnd) {
this.registrationEnd = registrationEnd;
}
public List<Team> getTeams() {
return Collections.unmodifiableList(teams);
}
@ -156,8 +135,4 @@ public class Tournament {
teams.add(team);
return SignupTeamResult.SUCCESS;
}
public List<BeerPongTable> getTables() {
return tables;
}
}

View file

@ -0,0 +1,4 @@
package at.ac.tuwien.sepr.groupphase.backend.exception;
public class BadTournamentSignupTokenException extends RuntimeException {
}

View file

@ -2,6 +2,7 @@ package at.ac.tuwien.sepr.groupphase.backend.service;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import at.ac.tuwien.sepr.groupphase.backend.entity.Team;
import org.springframework.security.access.AccessDeniedException;
@ -66,7 +67,9 @@ public interface TournamentService {
*
* @return created tournament entity
*/
SignupTeamResult signupTeamForTournament(long tournamentId, String name);
SignupTeamResult signupTeamForTournament(
long tournamentId, UUID selfRegistrationToken, String name
);
/**
* Find a single tournament entity by id.

View file

@ -5,10 +5,13 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import at.ac.tuwien.sepr.groupphase.backend.entity.Team;
import at.ac.tuwien.sepr.groupphase.backend.exception.TournamentAlreadyStartedException;
import at.ac.tuwien.sepr.groupphase.backend.exception.BadTournamentSignupTokenException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
@ -143,12 +146,18 @@ public class TournamentServiceImpl implements TournamentService {
@Override
@Transactional
public SignupTeamResult signupTeamForTournament(long tournamentId, String name) {
public SignupTeamResult signupTeamForTournament(
long tournamentId, UUID selfRegistrationToken, String name
) {
LOGGER.debug("Create new team {} for tournament {}", name, tournamentId);
final var tournament = tournamentRepository.findById(tournamentId)
.orElseThrow(() -> new IllegalArgumentException("Tournament not found"));
if (!Objects.equals(tournament.getSelfRegistrationToken(), selfRegistrationToken)) {
throw new BadTournamentSignupTokenException();
}
final var newTeam = new Team(name, tournament);
var result = tournament.signupTeam(newTeam);
if (result != SignupTeamResult.SUCCESS) {
@ -289,7 +298,7 @@ public class TournamentServiceImpl implements TournamentService {
if (!tournament.getId().equals(tournamentId)) {
throw new NotFoundException("Team not found in tournament");
}
var test = tournament.getQualificationMatches();
if ((long) tournament.getQualificationMatches().size() > 0) {
throw new TournamentAlreadyStartedException();
}

View file

@ -106,10 +106,10 @@ public class BeerPongTableEndpointTest extends TestUserData implements TestData
var user = new ApplicationUser("TestUser", "Password", false);
userRepository.save(user);
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", user
);
tournament = tournamentService.create(tournament, user.getUsername());
var tableDto = new CreateBeerPongTableDto();
@ -142,10 +142,10 @@ public class BeerPongTableEndpointTest extends TestUserData implements TestData
var user = new ApplicationUser("TestUser", "Password", false);
userRepository.save(user);
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", user
);
tournament = tournamentService.create(tournament, user.getUsername());
var tableDto = new CreateBeerPongTableDto();
@ -185,11 +185,11 @@ public class BeerPongTableEndpointTest extends TestUserData implements TestData
@Test
public void updateBeerPongTableForExistingTournamentThatWasCreatedByTheCurrentUser() throws Exception {
// setup
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
tournament = tournamentService.create(tournament, TestDataGenerator.TEST_USER);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournament = tournamentService.create(tournament, TEST_USER);
var beerPongTable = new BeerPongTable("TEST");
beerPongTable.setTournament(tournament);
@ -221,11 +221,11 @@ public class BeerPongTableEndpointTest extends TestUserData implements TestData
@Test
public void updateBeerPongTableForExistingTournamentThatWasntCreatedByTheCurrentUser() throws Exception {
// setup
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
tournament = tournamentService.create(tournament, TestDataGenerator.TEST_USER);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournament = tournamentService.create(tournament, TEST_USER);
var beerPongTable = new BeerPongTable("TEST");
beerPongTable.setTournament(tournament);

View file

@ -5,6 +5,7 @@ import at.ac.tuwien.sepr.groupphase.backend.basetest.TestUserData;
import at.ac.tuwien.sepr.groupphase.backend.config.properties.SecurityProperties;
import at.ac.tuwien.sepr.groupphase.backend.datagenerator.TestDataGenerator;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.CreateTournamentDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentCreateTeamDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentListDto;
import at.ac.tuwien.sepr.groupphase.backend.entity.ApplicationUser;
@ -18,6 +19,7 @@ import at.ac.tuwien.sepr.groupphase.backend.repository.UserRepository;
import at.ac.tuwien.sepr.groupphase.backend.security.JwtTokenizer;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
@ -126,12 +128,10 @@ public class TournamentEndpointTest extends TestUserData implements TestData {
@Test
public void successfullyGetTournaments() throws Exception {
// setup
var tournament = new Tournament();
tournament.setName("TOURNAMENT_WITHOUT_TEAMS");
tournament.setMaxParticipants(64l);
tournament.setOrganizer(userRepository.findByUsername(TestDataGenerator.TEST_USER));
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournamentRepository.saveAndFlush(tournament);
var mvcResult = this.mockMvc.perform(get(TOURNAMENT_BASE_URI)
@ -186,12 +186,10 @@ public class TournamentEndpointTest extends TestUserData implements TestData {
@Test
public void generateQualificationMatchesForTournamentWithEnoughTeams() throws Exception {
// setup
var tournament = new Tournament();
tournament.setName("TOURNAMENT_WITH_TEAMS");
tournament.setMaxParticipants(64l);
tournament.setOrganizer(userRepository.findByUsername(TestDataGenerator.TEST_USER));
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
var tournament = new Tournament(
"TOURNAMENT_WITHOUT_TEAMS", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
var numberOfTeams = 16;
for (int i = 0; i < numberOfTeams; i++) {
@ -234,12 +232,10 @@ public class TournamentEndpointTest extends TestUserData implements TestData {
@Test
public void generateQualificationMatchesForTournamentWithoutEnoughTeams() throws Exception {
// setup
var tournament = new Tournament();
tournament.setName("TOURNAMENT_WITHOUT_TEAMS");
tournament.setMaxParticipants(64l);
tournament.setOrganizer(userRepository.findByUsername(TestDataGenerator.TEST_USER));
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
var tournament = new Tournament(
"TOURNAMENT_WITHOUT_TEAMS", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournamentRepository.saveAndFlush(tournament);
var mvcResult = this.mockMvc
@ -260,12 +256,10 @@ public class TournamentEndpointTest extends TestUserData implements TestData {
@Test
public void generateQualificationMatchesForTournamentFromAnotherOrganizerWhenItIsntAllowed() throws Exception {
// setup
var tournament = new Tournament();
tournament.setName("TOURNAMENT_WITHOUT_TEAMS");
tournament.setMaxParticipants(64l);
tournament.setOrganizer(userRepository.findByUsername(TestDataGenerator.TEST_USER));
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
var tournament = new Tournament(
"TOURNAMENT_WITHOUT_TEAMS", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournamentRepository.saveAndFlush(tournament);
var mvcResult = this.mockMvc
@ -514,4 +508,77 @@ public class TournamentEndpointTest extends TestUserData implements TestData {
.andReturn();
});
}
@Test
public void cannotSelfSignupTeamForTournamentWithMissingToken() throws Exception {
final var tournament = new Tournament(
"FOOBAR", LocalDateTime.now().plusDays(5), 64L, "test tournament",
userRepository.findByUsername(TestDataGenerator.TEST_USER));
tournamentRepository.saveAndFlush(tournament);
var mvcResult = this.mockMvc.perform(get(TOURNAMENT_BASE_URI)
.header(securityProperties.getAuthHeader(), jwtTokenizer.getAuthToken(TEST_USER, TEST_USER_ROLES))
.contentType(MediaType.APPLICATION_JSON))
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
final var tournaments = objectMapper.readValue(response.getContentAsString(), TournamentListDto[].class);
assertThat(tournaments)
.isNotNull()
.hasSize(1)
.extracting("name")
.containsExactly(tournament.getName());
final var listDto = tournaments[0];
final var createDto = new TournamentCreateTeamDto("baz");
mvcResult = this.mockMvc.perform(post(TOURNAMENT_BASE_URI + "/{tournamentId}/teams", listDto.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createDto)))
.andDo(print())
.andReturn();
response = mvcResult.getResponse();
assertEquals(response.getStatus(), HttpStatus.UNAUTHORIZED.value());
assertEquals(response.getContentAsString(), "self registration token missing or incorrect");
}
@Test
public void cannotSelfSignupTeamForTournamentWithInvalidToken() throws Exception {
final var tournament = new Tournament(
"FOOBAR", LocalDateTime.now().plusDays(5), 64L, "test tournament",
userRepository.findByUsername(TestDataGenerator.TEST_USER));
tournamentRepository.saveAndFlush(tournament);
var mvcResult = this.mockMvc.perform(get(TOURNAMENT_BASE_URI)
.header(securityProperties.getAuthHeader(), jwtTokenizer.getAuthToken(TEST_USER, TEST_USER_ROLES))
.contentType(MediaType.APPLICATION_JSON))
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
final var tournaments = objectMapper.readValue(response.getContentAsString(), TournamentListDto[].class);
assertThat(tournaments)
.isNotNull()
.hasSize(1)
.extracting("name")
.containsExactly(tournament.getName());
final var listDto = tournaments[0];
final var createDto = new TournamentCreateTeamDto("baz");
mvcResult = this.mockMvc.perform(post(TOURNAMENT_BASE_URI + "/{tournamentId}/teams", listDto.getId())
.param("token", "11111111-2222-3333-4444-555555555555")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createDto)))
.andDo(print())
.andReturn();
response = mvcResult.getResponse();
assertEquals(response.getStatus(), HttpStatus.UNAUTHORIZED.value());
assertEquals(response.getContentAsString(), "self registration token missing or incorrect");
}
}

View file

@ -8,6 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import java.time.LocalDateTime;
import at.ac.tuwien.sepr.groupphase.backend.basetest.TestData;
import at.ac.tuwien.sepr.groupphase.backend.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@ -29,10 +31,12 @@ import at.ac.tuwien.sepr.groupphase.backend.service.TournamentService;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public class BeerPongTableServiceTest extends TestUserData {
public class BeerPongTableServiceTest extends TestUserData implements TestData {
@Autowired
private TournamentService tournamentService;
@Autowired
private UserRepository userRepository;
@Autowired
private BeerPongTableService beerPongTableService;
@Autowired
private BeerPongTableRepository beerPongTableRepository;
@ -40,11 +44,11 @@ public class BeerPongTableServiceTest extends TestUserData {
@Test
public void getSingleBeerPongTableById() {
// setup
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
tournament = tournamentService.create(tournament, TestDataGenerator.TEST_USER);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournament = tournamentService.create(tournament, TEST_USER);
var beerPongTable = new BeerPongTable("TEST");
beerPongTable.setTournament(tournament);
@ -68,11 +72,11 @@ public class BeerPongTableServiceTest extends TestUserData {
@Test
public void createNewBeerPongTableForExistingTournamentThatWasCreatedByTheCurrentUser() {
// setup
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
tournament = tournamentService.create(tournament, TestDataGenerator.TEST_USER);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournament = tournamentService.create(tournament, TEST_USER);
var tableDto = new CreateBeerPongTableDto();
tableDto.setName("TEST_NAME");
@ -89,11 +93,11 @@ public class BeerPongTableServiceTest extends TestUserData {
@Test
public void createNewBeerPongTableForExistingTournamentThatWasntCreatedByTheCurrentUser() {
// setup
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
tournament = tournamentService.create(tournament, TestDataGenerator.TEST_USER);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournament = tournamentService.create(tournament, TEST_USER);
var tableDto = new CreateBeerPongTableDto();
tableDto.setName("TEST_NAME");
@ -115,11 +119,11 @@ public class BeerPongTableServiceTest extends TestUserData {
@Test
public void updateBeerPongTableForExistingTournamentThatWasCreatedByTheCurrentUser() {
// setup
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
tournament = tournamentService.create(tournament, TestDataGenerator.TEST_USER);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournament = tournamentService.create(tournament, TEST_USER);
var beerPongTable = new BeerPongTable("TEST");
beerPongTable.setTournament(tournament);
@ -139,11 +143,11 @@ public class BeerPongTableServiceTest extends TestUserData {
@Test
public void updateBeerPongTableForExistingTournamentThatWasntCreatedByTheCurrentUser() {
// setup
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64l);
tournament = tournamentService.create(tournament, TestDataGenerator.TEST_USER);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournament = tournamentService.create(tournament, TEST_USER);
var beerPongTable = new BeerPongTable("TEST");
beerPongTable.setTournament(tournament);

View file

@ -53,10 +53,10 @@ public class TournamentServiceTest extends TestUserData implements TestData {
@Test
public void createNewTournamentWithTestUserAsOrganizer() {
var tournament = new Tournament();
tournament.setName("TEST_TOURNAMENT");
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournament.setMaxParticipants(64L);
var tournament = new Tournament(
"TEST_TOURNAMENT", LocalDateTime.now().plusDays(1), 64L,
"testdescription", userRepository.findByUsername(TEST_USER)
);
tournament = tournamentService.create(tournament, TestDataGenerator.TEST_USER);
assertNotNull(tournament);
@ -152,15 +152,18 @@ public class TournamentServiceTest extends TestUserData implements TestData {
}
Tournament generateTournamentWithFinishedQualiPhase() {
var tournament = new Tournament("testname", LocalDateTime.now().plusMinutes(1), 64L, "testdescription",
userRepository.findByUsername(TEST_USER));
tournamentService
.create(tournament, TEST_USER);
var tournament = new Tournament(
"testname", LocalDateTime.now().plusMinutes(1), 64L, "testdescription",
userRepository.findByUsername(TEST_USER));
tournamentService.create(tournament, TEST_USER);
IntStream.rangeClosed(1, 32).forEach(i -> {
assertEquals(SignupTeamResult.SUCCESS,
tournamentService.signupTeamForTournament(tournament.getId(), "team" + i));
assertEquals(
SignupTeamResult.SUCCESS,
tournamentService.signupTeamForTournament(
tournament.getId(), tournament.getSelfRegistrationToken(), "team" + i
)
);
});
tournamentService.generateQualificationMatchesForTournament(tournament.getId(), TEST_USER);

View file

@ -13,6 +13,20 @@ context('Get Tournaments', () => {
cy.get('[data-cy="tournaments-list"]').should('exist');
cy.get('[data-cy="tournaments-list-item"]').should('have.length.at.least', 1);
});
it('successfully copied team self-registration link', () => {
cy.fixture('settings').then(settings => {
cy.intercept('GET', '/api/v1/tournaments', { fixture: 'tournaments.json' }).as(
'getTournaments',
);
cy.visit('/#/tournaments');
cy.wait('@getTournaments');
cy.get('[data-cy="open-copy-team-self-reg-link-dialog"]').first().click();
cy.get('[data-cy="copy-button"]').first().click();
cy.assertClipboardContents(`http://${settings.baseUrl}/#/tournaments/1/signup`);
});
});
});
context('Delete Tournament', () => {

View file

@ -82,3 +82,11 @@ Cypress.Commands.add('fillTournamentCreateFormWithInvalidData', () => {
cy.contains('At least 16 participants are required.');
});
});
Cypress.Commands.add('assertClipboardContents', value => {
cy.window().then(window => {
window.navigator.clipboard.readText().then(text => {
expect(text).to.eq(value);
});
});
});

View file

@ -14,18 +14,18 @@ git_push.sh
index.ts
model/beerPongTableDto.ts
model/createBeerPongTableDto.ts
model/createTeamDto.ts
model/createTournamentDto.ts
model/detailedMessageDto.ts
model/messageInquiryDto.ts
model/models.ts
model/signupTeamResponseDto.ts
model/simpleMessageDto.ts
model/teamDto.ts
model/tournamentCreateTeamDto.ts
model/tournamentDto.ts
model/tournamentListDto.ts
model/tournamentQualificationMatchDto.ts
model/tournamentQualificationMatchParticipantDto.ts
model/tournamentSignupTeamResponseDto.ts
model/updateBeerPongTableDto.ts
model/userDetailDto.ts
model/userLoginDto.ts

View file

@ -18,20 +18,20 @@ 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 { TeamDto } from '../model/teamDto';
// @ts-ignore
import { TournamentCreateTeamDto } from '../model/tournamentCreateTeamDto';
// @ts-ignore
import { TournamentDto } from '../model/tournamentDto';
// @ts-ignore
import { TournamentListDto } from '../model/tournamentListDto';
// @ts-ignore
import { TournamentQualificationMatchDto } from '../model/tournamentQualificationMatchDto';
// @ts-ignore
import { TournamentSignupTeamResponseDto } from '../model/tournamentSignupTeamResponseDto';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
@ -565,19 +565,29 @@ export class TournamentEndpointService {
/**
* Create a new team
* @param tournamentId
* @param createTeamDto
* @param token
* @param tournamentCreateTeamDto
* @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> {
public signupTeamForTournament(tournamentId: number, token: string, tournamentCreateTeamDto: TournamentCreateTeamDto, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<TournamentSignupTeamResponseDto>;
public signupTeamForTournament(tournamentId: number, token: string, tournamentCreateTeamDto: TournamentCreateTeamDto, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<TournamentSignupTeamResponseDto>>;
public signupTeamForTournament(tournamentId: number, token: string, tournamentCreateTeamDto: TournamentCreateTeamDto, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<TournamentSignupTeamResponseDto>>;
public signupTeamForTournament(tournamentId: number, token: string, tournamentCreateTeamDto: TournamentCreateTeamDto, 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.');
if (token === null || token === undefined) {
throw new Error('Required parameter token was null or undefined when calling signupTeamForTournament.');
}
if (tournamentCreateTeamDto === null || tournamentCreateTeamDto === undefined) {
throw new Error('Required parameter tournamentCreateTeamDto was null or undefined when calling signupTeamForTournament.');
}
let localVarQueryParameters = new HttpParams({encoder: this.encoder});
if (token !== undefined && token !== null) {
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
<any>token, 'token');
}
let localVarHeaders = this.defaultHeaders;
@ -626,10 +636,11 @@ export class TournamentEndpointService {
}
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}`,
return this.httpClient.request<TournamentSignupTeamResponseDto>('post', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
body: createTeamDto,
body: tournamentCreateTeamDto,
params: localVarQueryParameters,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,

View file

@ -1,16 +1,16 @@
export * from './beerPongTableDto';
export * from './createBeerPongTableDto';
export * from './createTeamDto';
export * from './createTournamentDto';
export * from './detailedMessageDto';
export * from './messageInquiryDto';
export * from './signupTeamResponseDto';
export * from './simpleMessageDto';
export * from './teamDto';
export * from './tournamentCreateTeamDto';
export * from './tournamentDto';
export * from './tournamentListDto';
export * from './tournamentQualificationMatchDto';
export * from './tournamentQualificationMatchParticipantDto';
export * from './tournamentSignupTeamResponseDto';
export * from './updateBeerPongTableDto';
export * from './userDetailDto';
export * from './userLoginDto';

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 TournamentCreateTeamDto {
name: string;
}

View file

@ -17,5 +17,6 @@ export interface TournamentListDto {
registrationEnd: string;
maxParticipants: number;
description?: string;
selfRegistrationToken: string;
}

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 TournamentSignupTeamResponseDto {
signupTeamResult?: TournamentSignupTeamResponseDto.SignupTeamResultEnum;
}
export namespace TournamentSignupTeamResponseDto {
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

@ -26,6 +26,11 @@ const routes: Routes = [
{ path: 'details/:username', component: UserDetailComponent },
],
},
{
path: '',
component: MainLayoutComponent,
children: [{ path: 'tournaments/:tournamentId/signup', component: TeamSignupComponent }],
},
{
path: '',
component: MainLayoutComponent,
@ -40,10 +45,7 @@ const routes: Routes = [
{ path: 'create', component: TournamentCreateComponent },
{
path: ':tournamentId',
children: [
{ path: 'signup', component: TeamSignupComponent },
{ path: 'teams', component: TournamentTeamsComponent },
],
children: [{ path: 'teams', component: TournamentTeamsComponent }],
},
],
},

View file

@ -27,6 +27,7 @@ import { MainLayoutComponent } from './layouts/main-layout/main-layout.component
import { MatListItem, MatNavList } from '@angular/material/list';
import { MatDivider } from '@angular/material/divider';
import { SidebarComponent } from './components/sidebar/sidebar.component';
import { ClipboardModule } from '@angular/cdk/clipboard';
import localeDe from '@angular/common/locales/de';
import { registerLocaleData } from '@angular/common';
registerLocaleData(localeDe);
@ -36,6 +37,7 @@ import { CommonModule } from '@angular/common';
import { TeamSignupComponent } from './components/tournament/team-signup/team-signup.component';
import { TournamentsComponent } from './components/tournament/tournaments/tournaments.component';
import { TournamentCardComponent } from './components/tournament/tournament-card/tournament-card.component';
import { CopyLinkDialogComponent } from './components/copy-link-dialog/copy-link-dialog.component';
@NgModule({
declarations: [
@ -56,6 +58,7 @@ import { TournamentCardComponent } from './components/tournament/tournament-card
UpdateUserComponent,
TeamSignupComponent,
ConfirmDialogComponent,
CopyLinkDialogComponent,
],
imports: [
AppRoutingModule,
@ -79,6 +82,7 @@ import { TournamentCardComponent } from './components/tournament/tournament-card
MatButtonModule,
MatIconModule,
CommonModule,
ClipboardModule,
],
providers: [
httpInterceptorProviders,

View file

@ -0,0 +1,16 @@
<h2 matDialogTitle>Copy {{ title }}</h2>
<mat-dialog-content>
<mat-form-field>
<input matInput readonly="true" [value]="link" />
</mat-form-field>
<button
data-cy="copy-button"
mat-icon-button
aria-label="Copy link"
matTooltip="Copy link"
[cdkCopyToClipboard]="link"
(click)="showNotification()"
>
<mat-icon>insert_link</mat-icon>
</button>
</mat-dialog-content>

View file

@ -0,0 +1,11 @@
mat-dialog-content {
display: flex;
}
mat-form-field {
flex-grow: 1;
}
button {
margin-left: 16px;
}

View file

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

View file

@ -0,0 +1,32 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
export interface CopyLinkDialogData {
title: string;
link: string;
}
@Component({
selector: 'app-copy-link-dialog',
templateUrl: './copy-link-dialog.component.html',
styleUrl: './copy-link-dialog.component.scss',
})
export class CopyLinkDialogComponent {
title: string;
link: string;
constructor(
@Inject(MAT_DIALOG_DATA) data: CopyLinkDialogData,
private snackBar: MatSnackBar,
) {
this.title = data.title;
this.link = data.link;
}
showNotification(): void {
this.snackBar.open('Copied link to clipboard!', 'OK', {
duration: 2500,
});
}
}

View file

@ -1,6 +1,6 @@
<mat-nav-list>
<mat-list-item>
<h1>Beer Brawl</h1>
<a [routerLink]="'/'"><h1>Beer Brawl</h1></a>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item>

View file

@ -1,10 +1,11 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TournamentEndpointService } from '../../../../../openapi-generated/api/tournamentEndpoint.service';
import { Component } from '@angular/core';
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';
import { TournamentDto, TournamentSignupTeamResponseDto } from 'openapi-generated';
import { catchError, of } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TournamentEndpointService } from '@api';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-team-signup',
@ -13,6 +14,8 @@ import { extend } from 'lodash';
})
export class TeamSignupComponent {
tournament: TournamentDto | undefined;
token: string | null | undefined;
success = false;
nameFormControl = new FormControl('', [
Validators.minLength(3),
@ -33,41 +36,48 @@ export class TeamSignupComponent {
constructor(
private tournamentService: TournamentEndpointService,
private route: ActivatedRoute,
private snackBar: MatSnackBar,
) {
const tournamentId = Number(this.route.snapshot.paramMap.get('tournamentId'));
const token = this.route.snapshot.queryParamMap.get('token');
this.tournamentService.get(tournamentId).subscribe(tournament => {
this.tournament = tournament;
this.token = token;
});
}
async createTeam() {
const request = this.tournamentService.signupTeamForTournament(
this.tournament?.id!,
this.token!,
{ name: this.newTeamNameForm.controls.name.value! },
'response',
);
request
.pipe(
catchError(e => {
if (e.status !== 400) {
catchError((e: HttpErrorResponse) => {
if (!Object.values(TournamentSignupTeamResponseDto).includes(e.error)) {
const errorMessage =
typeof e.error === 'string' ? e.error : 'Failed to register team; invalid request';
this.snackBar.open(errorMessage, 'Dismiss', {
duration: 5000,
});
return [];
}
const response = e.error as SignupTeamResponseDto;
const response = e.error as TournamentSignupTeamResponseDto;
switch (response.signupTeamResult) {
case SignupTeamResponseDto.SignupTeamResultEnum.TeamAlreadyExists:
case TournamentSignupTeamResponseDto.SignupTeamResultEnum.TeamAlreadyExists:
this.nameFormControl.setErrors({ teamAlreadyExists: true });
return of();
case SignupTeamResponseDto.SignupTeamResultEnum.MaxParticipantsReached:
case TournamentSignupTeamResponseDto.SignupTeamResultEnum.MaxParticipantsReached:
this.nameFormControl.setErrors({ maxParticipantsReached: true });
return of();
default:
return [];
}
this.nameFormControl.setErrors({ nameAlreadyTaken: true });
return of();
}),
)
.subscribe(response => (this.success = true));
.subscribe(() => (this.success = true));
}
}

View file

@ -6,19 +6,26 @@
<button mat-icon-button>
<mat-icon>edit</mat-icon>
</button>
<button
data-cy="open-copy-team-self-reg-link-dialog"
mat-icon-button
aria-label="Copy team self registration link"
matTooltip="Copy team self registration link"
(click)="openCopyTeamRegistrationLink()"
>
<mat-icon>insert_link</mat-icon>
</button>
</div>
<div class="card-footer">
<p class="card-date">
registration ends at {{ tournament?.registrationEnd | date: 'short' }}
</p>
<div class="card-icons">
<button mat-icon-button>
<mat-icon>play_arrow</mat-icon>
</button>
<button mat-icon-button data-cy="delete-tournament-btn" (click)="openConfirmDialog()">
<mat-icon>delete</mat-icon>
</button>
</div>
<button mat-icon-button>
<mat-icon>play_arrow</mat-icon>
</button>
<button mat-icon-button data-cy="delete-tournament-btn" (click)="openConfirmDialog()">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</mat-card>

View file

@ -33,6 +33,7 @@
.card-title {
font-size: 1.25rem; /* Larger font size for the title */
font-weight: 500; /* Medium font weight for better readability */
flex-grow: 1;
}
.card-footer {
@ -44,14 +45,10 @@
.card-date {
font-size: 0.8rem; /* Default font size for the date */
color: #757575; /* Subtle color for the date */
flex-grow: 1;
}
.card-icons {
display: flex;
gap: 0.5rem;
}
.card-icons button {
button {
color: #757575; /* Default color for icons */
}

View file

@ -1,14 +1,15 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TournamentEndpointService, TournamentListDto } from '@api';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ConfirmationService } from '../../../services/confirmation.service';
import { DatePipe } from '@angular/common';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { CopyLinkService } from 'src/app/services/copy-link.service';
@Component({
selector: 'app-tournament-card',
templateUrl: './tournament-card.component.html',
styleUrls: ['./tournament-card.component.scss'],
providers: [Location, { provide: LocationStrategy, useClass: PathLocationStrategy }],
})
export class TournamentCardComponent {
@Input() tournament: TournamentListDto | undefined;
@ -18,6 +19,8 @@ export class TournamentCardComponent {
private tournamentService: TournamentEndpointService,
private snackBar: MatSnackBar,
private confirmationService: ConfirmationService,
private copyLinkService: CopyLinkService,
private location: Location,
) {}
async openConfirmDialog(): Promise<void> {
@ -44,4 +47,12 @@ export class TournamentCardComponent {
});
}
}
openCopyTeamRegistrationLink(): void {
const link = this.location.prepareExternalUrl(
`#/tournaments/${this.tournament?.id}/signup?token=${this.tournament?.selfRegistrationToken}`,
);
this.copyLinkService.openDialog({ title: 'Team self-registration link', link });
}
}

View file

@ -0,0 +1,25 @@
import { inject, Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import {
CopyLinkDialogComponent,
CopyLinkDialogData,
} from '../components/copy-link-dialog/copy-link-dialog.component';
@Injectable({
providedIn: 'root',
})
export class CopyLinkService {
matDialog = inject(MatDialog);
/*
* Opens a dialog and prompts the user to copy a given link.
* @param data dialog data to use
*/
openDialog(data: CopyLinkDialogData): void {
this.matDialog.open(CopyLinkDialogComponent, {
width: '750px',
maxWidth: '900px',
data,
});
}
}

View file

@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ConfirmationService } from './confirmation.service';
describe('DeleteConfirmationService', () => {
let service: ConfirmationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ConfirmationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});