NumbersLot

This commit is contained in:
Patrick Haßel 2024-11-06 14:40:27 +01:00
parent bff407c446
commit a1e3a4353d
26 changed files with 218 additions and 142 deletions

View File

@ -1,14 +1,15 @@
import {Group} from "../../group/Group";
import {validateDate, validateDateOrNull, validateNumberOrNull, validateString} from "../../common/validators";
import {validateDate, validateList, validateString} from "../../common/validators";
import {NumbersLot} from "./NumbersLot";
import {UserPrivate} from "../../User/UserPrivate";
export class Numbers {
constructor(
readonly uuid: string,
readonly group: Group,
readonly date: Date,
readonly read: Date | null,
readonly number: number | null,
readonly lots: NumbersLot[],
readonly group: Group,
) {
// -
}
@ -16,11 +17,14 @@ export class Numbers {
static fromJson(json: any): Numbers {
return new Numbers(
validateString(json['uuid']),
Group.fromJson(json['group']),
validateDate(json['date']),
validateDateOrNull(json['read']),
validateNumberOrNull(json['number']),
validateList(json['lots'], NumbersLot.fromJson),
Group.fromJson(json['group']),
);
}
getMine(user: UserPrivate | null): NumbersLot | null {
return this.lots.filter(u => u.user.is(user))[0];
}
}

View File

@ -0,0 +1,20 @@
import {UserPublic} from "../../User/UserPublic";
import {validateNumberOrNull} from "../../common/validators";
export class NumbersLot {
constructor(
readonly user: UserPublic,
readonly number: number | null,
) {
// -
}
static fromJson(json: any): NumbersLot {
return new NumbersLot(
UserPublic.fromJson(json['user']),
validateNumberOrNull(json['number']),
);
}
}

View File

@ -28,8 +28,8 @@ export class NumbersService {
this.api.getPage(['Numbers', 'page', groupUuid, page, pageSize], Numbers.fromJson, next);
}
fetchAndMarkAsRead(numbersUuid: string, next: Next<Numbers>): void {
this.api.postSingle(['Numbers', 'fetchAndMarkAsRead'], numbersUuid, Numbers.fromJson, next);
byUuid(numbersUuid: string, next: Next<Numbers>): void {
this.api.postSingle(['Numbers', 'byUuid'], numbersUuid, Numbers.fromJson, next);
}
canAccess(numbersUuid: string, next: Next<boolean>): void {

View File

@ -118,9 +118,9 @@
</div>
<div class="numbers">
<table>
<tr class="number" *ngFor="let numbers of numbersList.content" (click)="numbersService.goto(numbers)" [class.read]="numbers.read !== null" [class.unread]="numbers.read === null">
<tr class="number" *ngFor="let numbers of numbersList.content" (click)="numbersService.goto(numbers)">
<td>{{ numbers.date | relative:now }}</td>
<td>{{ numbers.number || '-' }}</td>
<td>{{ numbers.getMine(userService.user)?.number || '-' }}</td>
</tr>
</table>
</div>

View File

@ -107,6 +107,9 @@ export class GroupComponent implements OnInit, OnDestroy {
this.userSubs.length = 0;
if (this.group !== null) {
this.group.users.forEach(_ => this.userSubs.push(this.userService.subscribePush(UserPublic, u => this.updateUser(u))));
this.updateNumbersList();
} else {
this.numbersList = Page.EMPTY;
}
}

View File

@ -51,8 +51,9 @@ export class ProfileComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.updateGroupList();
this.subs.push(this.userService.subscribePush(UserPrivate, _ => {
this.groupService.findAllJoined(groups => this.groups = groups);
this.updateGroupList();
}));
}
@ -61,6 +62,10 @@ export class ProfileComponent implements OnInit, OnDestroy {
this.subs.length = 0;
}
private updateGroupList() {
this.groupService.findAllJoined(groups => this.groups = groups);
}
protected nameValidator(name: string): boolean {
return name.length >= USER_NAME_MIN_LENGTH && !/\s+|^[^a-zA-Z0-9]+$/.test(name);
}

View File

@ -10,7 +10,7 @@
</div>
<div class="huge" (click)="gotoGroup()">
{{ numbers.number || '-' }}
{{ numbers.getMine(userService.user)?.number || '-' }}
</div>
<div class="buttons">

View File

@ -46,7 +46,7 @@ export class NumbersComponent implements OnInit, OnDestroy {
if (uuid) {
this.numbersService.canAccess(uuid, granted => {
if (granted) {
this.numbersService.fetchAndMarkAsRead(uuid, numbers => this.numbers = numbers);
this.numbersService.byUuid(uuid, numbers => this.numbers = numbers);
} else {
this.numbersService.getGroupUuid(uuid, groupUuid => this.groupService.goto(groupUuid));
}

View File

@ -79,7 +79,7 @@ public class Group extends GroupAbstract implements IWebSocketMessage {
@Column(nullable = false)
private ZonedDateTime lastAccess = created;
protected Group(@NonNull final User owner) {
public Group(@NonNull final User owner) {
this.owner = owner;
}

View File

@ -14,25 +14,25 @@ import java.util.Set;
@RestController
@RequiredArgsConstructor
@RequestMapping("Group")
public class GroupReadController {
public class GroupController {
private final GroupReadService groupReadService;
private final GroupService groupService;
@PostMapping("get")
public GroupDto get(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid) {
return groupReadService.get(privateUuid, groupUuid);
return groupService.get(privateUuid, groupUuid);
}
@NonNull
@GetMapping("findAllJoined")
public Set<GroupDto> findAllJoined(@NonNull final UserPrivateUuid userUuid) {
return groupReadService.findAllJoined(userUuid);
return groupService.findAllJoined(userUuid);
}
@NonNull
@PostMapping("findAllCommon")
public Set<GroupDto> findAllCommon(@NonNull final UserPrivateUuid userUuid, @NonNull final UserPublicUuid targetUuid) {
return groupReadService.findAllCommon(userUuid, targetUuid);
return groupService.findAllCommon(userUuid, targetUuid);
}
}

View File

@ -1,5 +1,7 @@
package de.ph87.tools.group;
import de.ph87.tools.group.access.GroupAccess;
import de.ph87.tools.group.access.GroupAccessService;
import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.uuid.GroupUuid;
import de.ph87.tools.user.User;
@ -22,7 +24,7 @@ import java.util.stream.Collectors;
@Service
@Transactional
@RequiredArgsConstructor
public class GroupReadService {
public class GroupService {
private final GroupAccessService groupAccessService;

View File

@ -1,12 +1,13 @@
package de.ph87.tools.group;
package de.ph87.tools.group.access;
import de.ph87.tools.group.Group;
import de.ph87.tools.user.User;
import lombok.Data;
@Data
public class GroupAccess {
public final User user;
public final User principal;
public final Group group;

View File

@ -1,4 +1,4 @@
package de.ph87.tools.group;
package de.ph87.tools.group.access;
import de.ph87.tools.group.uuid.GroupUuid;
import de.ph87.tools.user.uuid.UserPrivateUuid;

View File

@ -1,5 +1,7 @@
package de.ph87.tools.group;
package de.ph87.tools.group.access;
import de.ph87.tools.group.Group;
import de.ph87.tools.group.GroupRepository;
import de.ph87.tools.group.uuid.GroupUuid;
import de.ph87.tools.user.User;
import de.ph87.tools.user.UserAccessService;
@ -42,7 +44,7 @@ public class GroupAccessService {
@NonNull
public GroupAccess ownerAccess(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) {
final GroupAccess groupAccess = access(userPrivateUuid, groupUuid);
if (!groupAccess.group.isOwnedBy(groupAccess.user)) {
if (!groupAccess.group.isOwnedBy(groupAccess.principal)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
return groupAccess;

View File

@ -1,4 +1,4 @@
package de.ph87.tools.group;
package de.ph87.tools.group.member;
import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.requests.GroupJoinRequest;

View File

@ -1,5 +1,8 @@
package de.ph87.tools.group;
package de.ph87.tools.group.member;
import de.ph87.tools.group.Group;
import de.ph87.tools.group.GroupMapper;
import de.ph87.tools.group.GroupService;
import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.events.GroupLeftEvent;
import de.ph87.tools.group.requests.GroupJoinRequest;
@ -29,7 +32,7 @@ public class GroupMemberService {
private final UserService userService;
private final GroupReadService groupReadService;
private final GroupService groupService;
private final UserPushService userPushService;
@ -40,7 +43,7 @@ public class GroupMemberService {
@NonNull
public GroupDto join(@Nullable final UserPrivateUuid privateUuid, @NonNull final GroupJoinRequest request, @NonNull final HttpServletResponse response) {
final User user = userService.getUserByPrivateUuidOrElseCreate(privateUuid, response);
final Group group = groupReadService.getGroupByGroupUuid(request.groupUuid);
final Group group = groupService.getGroupByGroupUuid(request.groupUuid);
if (group.isBanned(user)) {
log.error("User is banned from Group and cannot join it: user={}, group={}", user, group);
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
@ -54,7 +57,7 @@ public class GroupMemberService {
public void leave(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid) {
final User user = userAccessService.access(privateUuid);
final Group group = groupReadService.getGroupByGroupUuid(groupUuid);
final Group group = groupService.getGroupByGroupUuid(groupUuid);
if (group.isOwnedBy(user)) {
// owner cannot remove itself from group
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);

View File

@ -1,4 +1,4 @@
package de.ph87.tools.group;
package de.ph87.tools.group.owner;
import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.requests.GroupChangePasswordRequest;

View File

@ -1,7 +1,13 @@
package de.ph87.tools.group;
package de.ph87.tools.group.owner;
import de.ph87.tools.group.Group;
import de.ph87.tools.group.GroupMapper;
import de.ph87.tools.group.GroupRepository;
import de.ph87.tools.group.access.GroupAccess;
import de.ph87.tools.group.access.GroupAccessService;
import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.events.GroupDeletedEvent;
import de.ph87.tools.group.member.GroupMemberService;
import de.ph87.tools.group.requests.GroupChangePasswordRequest;
import de.ph87.tools.group.requests.GroupChangeTitleRequest;
import de.ph87.tools.group.requests.GroupUserRequest;
@ -84,7 +90,7 @@ public class GroupOwnerService {
@NonNull
public GroupDto changeTitle(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupChangeTitleRequest request) {
final GroupAccess ug = groupAccessService.access(privateUuid, request.groupUuid);
if (!ug.group.isOwnedBy(ug.user)) {
if (!ug.group.isOwnedBy(ug.principal)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
ug.group.setTitle(request.title);
@ -111,7 +117,7 @@ public class GroupOwnerService {
private OwnerRemoveResult _removeUser(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request, @NonNull final BiConsumer<Group, User> beforePush) {
final GroupAccess access = groupAccessService.ownerAccess(privateUuid, request.groupUuid);
final User user = access.group.getUsers().stream().filter(u -> u.getPublicUuid().equals(request.userPublicUuid)).findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
if (user.equals(access.user)) {
if (user.equals(access.principal)) {
// owner cannot kick itself from group
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}

View File

@ -1,16 +1,15 @@
package de.ph87.tools.tools.numbers;
import de.ph87.tools.group.Group;
import de.ph87.tools.tools.numbers.lot.NumberLot;
import de.ph87.tools.tools.numbers.uuid.NumbersAbstract;
import de.ph87.tools.tools.numbers.uuid.NumbersUuid;
import de.ph87.tools.user.User;
import jakarta.annotation.Nullable;
import jakarta.persistence.*;
import lombok.*;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.UUID;
import java.util.*;
@Entity
@Getter
@ -44,40 +43,22 @@ public class Numbers extends NumbersAbstract {
@Column(nullable = false)
private ZonedDateTime date = ZonedDateTime.now();
@Column
@Nullable
private ZonedDateTime read = null;
@NonNull
@ManyToMany
@OrderColumn
private List<User> users;
@ElementCollection
private List<NumberLot> lots = new ArrayList<>();
public Numbers(@NonNull final Group group, @NonNull final List<User> users) {
public Numbers(@NonNull final Group group) {
this.group = group;
this.users = users;
createRandomLots(group.getUsers());
}
public void setRead(@NonNull final ZonedDateTime date) {
if (this.read != null) {
throw new RuntimeException();
private void createRandomLots(@NonNull final Set<User> users) {
lots.clear();
final List<User> shuffledUsers = new ArrayList<>(users);
Collections.shuffle(shuffledUsers);
for (int index = 0; index < shuffledUsers.size(); index++) {
lots.add(new NumberLot(shuffledUsers.get(index), index + 1));
}
this.read = date;
}
@Nullable
public Integer getNumberForUser(@NonNull final User user) {
for (int index = 0; index < users.size(); index++) {
final User userAtIndex = users.get(index);
if (user.equals(userAtIndex)) {
return index + 1;
}
}
return null;
}
public boolean containsUser(@NonNull final User user) {
return users.stream().anyMatch(user::equals);
}
}

View File

@ -2,19 +2,22 @@ package de.ph87.tools.tools.numbers;
import de.ph87.tools.user.User;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
@Getter
@ToString
class NumbersAccess {
public class NumbersAccess {
@NonNull
public final Numbers numbers;
public final User user;
@NonNull
public final User principal;
public NumbersAccess(final Numbers numbers, final User user) {
public NumbersAccess(@NonNull final User principal, @NonNull final Numbers numbers) {
this.numbers = numbers;
this.user = user;
this.principal = principal;
}
}

View File

@ -41,9 +41,9 @@ public class NumbersController {
}
@NonNull
@PostMapping("fetchAndMarkAsRead")
public NumbersDto fetchAndMarkAsRead(@NonNull final UserPrivateUuid privateUuid, final NumbersUuid numbersUuid) {
return numbersService.fetchAndMarkAsRead(privateUuid, numbersUuid);
@PostMapping("byUuid")
public NumbersDto byUuid(@NonNull final UserPrivateUuid privateUuid, final NumbersUuid numbersUuid) {
return numbersService.byUuid(privateUuid, numbersUuid);
}
}

View File

@ -3,14 +3,16 @@ package de.ph87.tools.tools.numbers;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import de.ph87.tools.common.uuid.UuidSerializer;
import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.tools.numbers.lot.NumberLotDto;
import de.ph87.tools.tools.numbers.uuid.NumbersAbstract;
import de.ph87.tools.tools.numbers.uuid.NumbersUuid;
import jakarta.annotation.Nullable;
import jakarta.persistence.ElementCollection;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.time.ZonedDateTime;
import java.util.List;
@Getter
@ToString(callSuper = true)
@ -20,24 +22,21 @@ public class NumbersDto extends NumbersAbstract {
@JsonSerialize(using = UuidSerializer.class)
private final NumbersUuid uuid;
@NonNull
private final GroupDto group;
@NonNull
private final ZonedDateTime date;
@Nullable
private final ZonedDateTime read;
@NonNull
@ElementCollection
private final List<NumberLotDto> lots;
@Nullable
private final Integer number;
@NonNull
private final GroupDto group;
public NumbersDto(@NonNull final Numbers numbers, @NonNull final GroupDto group, @Nullable final Integer number) {
this.uuid = numbers.getUuid();
this.date = numbers.getDate();
this.read = numbers.getRead();
public NumbersDto(@NonNull final NumbersAccess lotAccess, @NonNull final GroupDto group) {
this.uuid = lotAccess.numbers.getUuid();
this.date = lotAccess.numbers.getDate();
this.lots = lotAccess.numbers.getLots().stream().map(lot -> new NumberLotDto(lot, lotAccess.principal)).toList();
this.group = group;
this.number = number;
}
}

View File

@ -6,8 +6,6 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.ListCrudRepository;
import java.util.List;
public interface NumbersRepository extends ListCrudRepository<Numbers, String> {
@NonNull
@ -15,6 +13,4 @@ public interface NumbersRepository extends ListCrudRepository<Numbers, String> {
void deleteAllByGroup(@NonNull Group group);
List<Numbers> findAllByGroupAndReadNull(@NonNull Group group);
}

View File

@ -1,7 +1,7 @@
package de.ph87.tools.tools.numbers;
import de.ph87.tools.group.GroupAccess;
import de.ph87.tools.group.GroupAccessService;
import de.ph87.tools.group.access.GroupAccess;
import de.ph87.tools.group.access.GroupAccessService;
import de.ph87.tools.group.GroupMapper;
import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.uuid.GroupUuid;
@ -21,10 +21,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@Slf4j
@ -45,36 +41,13 @@ public class NumbersService {
public void create(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) {
final GroupAccess access = groupAccessService.ownerAccess(userPrivateUuid, groupUuid);
final List<User> users = new ArrayList<>(access.getGroup().getUsers());
Collections.shuffle(users);
final Numbers numbers = numbersRepository.save(new Numbers(access.getGroup(), users));
publish(numbers);
}
private void publish(@NonNull final Numbers numbers) {
numbers.getUsers()
.stream()
.filter(Objects::nonNull)
.forEach(user -> publish(numbers, user));
}
private void publish(@NonNull final Numbers numbers, @NonNull final User user) {
final NumbersDto dto = toDto(numbers, user);
log.debug("Sending event: {}", dto);
userPushService.push(user, dto);
}
@NonNull
public NumbersDto toDto(@NonNull final Numbers numbers, @NonNull final User user) {
final GroupDto group = groupMapper.toDto(numbers.getGroup());
final Integer number = numbers.getNumberForUser(user);
return new NumbersDto(numbers, group, number);
final Numbers numbers = numbersRepository.save(new Numbers(access.group));
pushAllLots(numbers);
}
public boolean canAccess(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) {
final User user = userAccessService.access(privateUuid);
final Numbers numbers = numbersRepository.findById(numbersUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
return numbers.containsUser(user);
final NumbersAccess access = access(privateUuid, numbersUuid);
return access.numbers.getGroup().getUsers().contains(access.principal);
}
@NonNull
@ -84,28 +57,44 @@ public class NumbersService {
}
@NonNull
public NumbersDto fetchAndMarkAsRead(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) {
final NumbersAccess access = access(privateUuid, numbersUuid);
final ZonedDateTime now = ZonedDateTime.now();
numbersRepository.findAllByGroupAndReadNull(access.numbers.getGroup()).forEach(numbers -> numbers.setRead(now));
return toDto(access.numbers, access.user);
}
@NonNull
private NumbersAccess access(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) {
final User user = userAccessService.access(privateUuid);
final Numbers numbers = numbersRepository.findById(numbersUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
if (!numbers.containsUser(user)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
return new NumbersAccess(numbers, user);
public NumbersDto byUuid(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) {
final NumbersAccess lotAccess = access(privateUuid, numbersUuid);
return toDto(lotAccess);
}
@NonNull
public Page<NumbersDto> page(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid, final int page, final int pageSize) {
final GroupAccess access = groupAccessService.access(privateUuid, groupUuid);
final GroupAccess groupAccess = groupAccessService.access(privateUuid, groupUuid);
final PageRequest pageable = PageRequest.of(page, pageSize, Sort.by(new Sort.Order(Sort.Direction.DESC, "date")));
return numbersRepository.findAllByGroup(access.group, pageable).map(numbers -> toDto(numbers, access.user));
return numbersRepository.findAllByGroup(groupAccess.group, pageable).map(numbers -> toDto(new NumbersAccess(groupAccess.principal, numbers)));
}
/* ACCESS --------------------------------------------------------------------------------------- */
@NonNull
private NumbersAccess access(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) {
final User principal = userAccessService.access(privateUuid);
final Numbers numbers = numbersRepository.findById(numbersUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
if (!numbers.getGroup().getUsers().contains(principal)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
return new NumbersAccess(principal, numbers);
}
/* PUSH, DTO ------------------------------------------------------------------------------------ */
private void pushAllLots(@NonNull final Numbers numbers) {
numbers.getLots()
.stream()
.filter(Objects::nonNull)
.map(lot -> new NumbersAccess(lot.getUser(), numbers))
.forEach(lotAccess -> userPushService.push(lotAccess.principal, toDto(lotAccess)));
}
@NonNull
private NumbersDto toDto(@NonNull final NumbersAccess lotAccess) {
final GroupDto group = groupMapper.toDto(lotAccess.numbers.getGroup());
return new NumbersDto(lotAccess, group);
}
}

View File

@ -0,0 +1,37 @@
package de.ph87.tools.tools.numbers.lot;
import de.ph87.tools.user.User;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.ToString;
@Getter
@ToString
@Embeddable
@NoArgsConstructor
public class NumberLot {
@NonNull
@ManyToOne(optional = false)
private User user;
@Column(nullable = false)
private int number;
@Column(nullable = false)
private boolean revealed = false;
public NumberLot(@NonNull final User user, final int number) {
this.user = user;
this.number = number;
}
public boolean is(@NonNull final User user) {
return this.user.equals(user);
}
}

View File

@ -0,0 +1,25 @@
package de.ph87.tools.tools.numbers.lot;
import de.ph87.tools.user.User;
import de.ph87.tools.user.UserPublicDto;
import jakarta.annotation.Nullable;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
@Getter
@ToString
public class NumberLotDto {
@NonNull
private final UserPublicDto user;
@Nullable
private final Integer number;
public NumberLotDto(@NonNull final NumberLot lot, @NonNull final User principal) {
this.user = new UserPublicDto(lot.getUser());
this.number = lot.isRevealed() || lot.getUser().equals(principal) ? lot.getNumber() : null;
}
}