1

Merge pull request #18 from ChUrl/FEATURE-testing

Feature testing
This commit is contained in:
Christoph
2020-04-22 14:35:56 +02:00
committed by GitHub
81 changed files with 2998 additions and 805 deletions

View File

@ -5,6 +5,6 @@ CREATE TABLE event
group_version INT NOT NULL,
exec_id VARCHAR(32) NOT NULL,
target_id VARCHAR(32),
event_type VARCHAR(16) NOT NULL,
event_date DATETIME NOT NULL,
event_payload JSON NOT NULL
);

View File

@ -1,3 +1,5 @@
DROP TABLE IF EXISTS event;
CREATE TABLE event
(
event_id INT PRIMARY KEY AUTO_INCREMENT,
@ -5,6 +7,6 @@ CREATE TABLE event
group_version INT NOT NULL,
exec_id VARCHAR(32) NOT NULL,
target_id VARCHAR(32),
event_type VARCHAR(16) NOT NULL,
event_date DATETIME NOT NULL,
event_payload TEXT NOT NULL
);

View File

@ -22,10 +22,10 @@ public class KeycloakConfig {
@Value("${keycloak.resource}")
private String clientId;
@Value("f73354fd-614e-4e24-b801-a8d0f4abf531")
@Value("2e2e5770-c454-4d31-be99-9d8c34c93089")
private String clientSecret;
@Value("https://gruppenkeycloak.herokuapp.com/auth/realms/master/protocol/openid-connect/token")
@Value("https://churl-keycloak.herokuapp.com/auth/realms/Gruppen/protocol/openid-connect/token")
private String tokenUri;
@Bean

View File

@ -6,7 +6,7 @@ import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.GroupFullException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.exception.UserAlreadyExistsException;
import mops.gruppen2.domain.exception.UserExistsException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.infrastructure.GroupCache;
@ -39,12 +39,17 @@ public class AddMemberEvent extends Event {
}
@Override
protected void applyEvent(Group group) throws UserAlreadyExistsException, GroupFullException {
protected void applyEvent(Group group) throws UserExistsException, GroupFullException {
group.addMember(target, user);
log.trace("\t\t\t\t\tNeue Members: {}", group.getMembers());
}
@Override
public String format() {
return "Benutzer hinzugefügt: " + target + ".";
}
@Override
public String type() {
return EventType.ADDMEMBER.toString();

View File

@ -39,6 +39,11 @@ public class CreateGroupEvent extends Event {
log.trace("\t\t\t\t\tNeue Gruppe: {}", group.toString());
}
@Override
public String format() {
return "Gruppe erstellt.";
}
@Override
public String type() {
return EventType.CREATEGROUP.toString();

View File

@ -20,7 +20,7 @@ public class DestroyGroupEvent extends Event {
@Override
protected void updateCache(GroupCache cache, Group group) {
cache.groupsRemove(group);
cache.groupsRemove(groupid, group);
}
@Override
@ -30,6 +30,11 @@ public class DestroyGroupEvent extends Event {
log.trace("\t\t\t\t\tGelöschte Gruppe: {}", group.toString());
}
@Override
public String format() {
return "Gruppe gelöscht.";
}
@Override
public String type() {
return EventType.DESTROYGROUP.toString();

View File

@ -13,21 +13,26 @@ import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.GroupCache;
import java.time.LocalDateTime;
import java.util.UUID;
import static com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
@Log4j2
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "class")
@JsonSubTypes({@JsonSubTypes.Type(value = AddMemberEvent.class, name = "ADDMEMBER"),
@JsonSubTypes.Type(value = CreateGroupEvent.class, name = "CREATEGROUP"),
@JsonSubTypes.Type(value = DestroyGroupEvent.class, name = "DESTROYGROUP"),
@JsonSubTypes.Type(value = KickMemberEvent.class, name = "KICKMEMBER"),
@JsonSubTypes.Type(value = SetDescriptionEvent.class, name = "SETDESCRIPTION"),
@JsonSubTypes.Type(value = SetInviteLinkEvent.class, name = "SETLINK"),
@JsonSubTypes.Type(value = SetLimitEvent.class, name = "SETLIMIT"),
@JsonSubTypes.Type(value = SetParentEvent.class, name = "SETPARENT"),
@JsonSubTypes.Type(value = SetTitleEvent.class, name = "SETTITLE"),
@JsonSubTypes.Type(value = SetTypeEvent.class, name = "SETTYPE"),
@JsonSubTypes.Type(value = UpdateRoleEvent.class, name = "UPDATEROLE")})
@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "class")
@JsonSubTypes({@Type(value = AddMemberEvent.class, name = "ADDMEMBER"),
@Type(value = CreateGroupEvent.class, name = "CREATEGROUP"),
@Type(value = DestroyGroupEvent.class, name = "DESTROYGROUP"),
@Type(value = KickMemberEvent.class, name = "KICKMEMBER"),
@Type(value = SetDescriptionEvent.class, name = "SETDESCRIPTION"),
@Type(value = SetInviteLinkEvent.class, name = "SETLINK"),
@Type(value = SetLimitEvent.class, name = "SETLIMIT"),
@Type(value = SetParentEvent.class, name = "SETPARENT"),
@Type(value = SetTitleEvent.class, name = "SETTITLE"),
@Type(value = SetTypeEvent.class, name = "SETTYPE"),
@Type(value = UpdateRoleEvent.class, name = "UPDATEROLE")})
@Getter
@NoArgsConstructor // Lombok needs a default constructor in the base class
public abstract class Event {
@ -44,6 +49,10 @@ public abstract class Event {
@JsonProperty("target")
protected String target;
@JsonProperty("date")
protected LocalDateTime date;
//TODO: Eigentlich sollte die Gruppe aus dem Cache genommen werden, nicht übergeben
public Event(UUID groupid, String exec, String target) {
this.groupid = groupid;
this.exec = exec;
@ -54,6 +63,7 @@ public abstract class Event {
if (this.version != 0) {
throw new BadArgumentException("Event wurde schon initialisiert. (" + type() + ")");
}
date = LocalDateTime.now();
log.trace("Event wurde initialisiert. (" + type() + "," + version + ")");
@ -68,9 +78,22 @@ public abstract class Event {
}
checkGroupIdMatch(group.getId());
updateCache(cache, group);
group.updateVersion(version);
applyEvent(group);
updateCache(cache, group); // Update erst nachdem apply keine exception geworfen hat
}
public void apply(Group group) throws EventException {
log.trace("Event wird angewendet:\t{}", this);
if (version == 0) {
throw new BadArgumentException("Event wurde nicht initialisiert.");
}
checkGroupIdMatch(group.getId());
group.updateVersion(version);
applyEvent(group);
}
private void checkGroupIdMatch(UUID groupid) throws IdMismatchException {
@ -88,6 +111,9 @@ public abstract class Event {
protected abstract void applyEvent(Group group) throws EventException;
@JsonIgnore
public abstract String format();
@JsonIgnore
public abstract String type();
}

View File

@ -34,6 +34,11 @@ public class KickMemberEvent extends Event {
log.trace("\t\t\t\t\tNeue Members: {}", group.getMembers());
}
@Override
public String format() {
return "Mitglied entfernt: " + target + ".";
}
@Override
public String type() {
return EventType.KICKMEMBER.toString();

View File

@ -38,6 +38,11 @@ public class SetDescriptionEvent extends Event {
log.trace("\t\t\t\t\tNeue Beschreibung: {}", group.getDescription());
}
@Override
public String format() {
return "Beschreibung gesetzt: " + description + ".";
}
@Override
public String type() {
return EventType.SETDESCRIPTION.toString();

View File

@ -38,6 +38,11 @@ public class SetInviteLinkEvent extends Event {
log.trace("\t\t\t\t\tNeuer Link: {}", group.getLink());
}
@Override
public String format() {
return "Einladungslink gesetzt: " + link + ".";
}
@Override
public String type() {
return EventType.SETLINK.toString();

View File

@ -36,6 +36,11 @@ public class SetLimitEvent extends Event {
log.trace("\t\t\t\t\tNeues UserLimit: {}", group.getLimit());
}
@Override
public String format() {
return "Benutzerlimit gesetzt: " + limit + ".";
}
@Override
public String type() {
return EventType.SETLIMIT.toString();

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.wrapper.Parent;
@ -29,12 +30,17 @@ public class SetParentEvent extends Event {
protected void updateCache(GroupCache cache, Group group) {}
@Override
protected void applyEvent(Group group) throws NoAccessException {
protected void applyEvent(Group group) throws NoAccessException, BadArgumentException {
group.setParent(exec, parent);
log.trace("\t\t\t\t\tNeues Parent: {}", group.getParent());
}
@Override
public String format() {
return "Veranstaltungszugehörigkeit gesetzt: " + parent.getValue() + ".";
}
@Override
public String type() {
return EventType.SETPARENT.toString();

View File

@ -38,6 +38,11 @@ public class SetTitleEvent extends Event {
log.trace("\t\t\t\t\tNeuer Titel: {}", group.getTitle());
}
@Override
public String format() {
return "Titel gesetzt: " + title + ".";
}
@Override
public String type() {
return EventType.SETTITLE.toString();

View File

@ -3,6 +3,7 @@ package mops.gruppen2.domain.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.experimental.NonFinal;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.model.group.Group;
@ -20,6 +21,12 @@ public class SetTypeEvent extends Event {
@JsonProperty("type")
Type type;
//TODO: blöder hack, das soll eigentlich anders gehen
// Problem ist, dass die Gruppe vor dem Cache verändert wird, also kann der cache den alten Typ
// nicht mehr aus der Gruppe holen
@NonFinal
Type oldType;
public SetTypeEvent(UUID groupId, String exec, @Valid Type type) {
super(groupId, exec, null);
@ -28,15 +35,21 @@ public class SetTypeEvent extends Event {
@Override
protected void updateCache(GroupCache cache, Group group) {
cache.typesRemove(group);
cache.typesRemove(oldType, group);
cache.typesPut(type, group);
}
@Override
protected void applyEvent(Group group) throws EventException {
oldType = group.getType();
group.setType(exec, type);
}
@Override
public String format() {
return "Gruppentype gesetzt: " + type + ".";
}
@Override
public String type() {
return EventType.SETTYPE.toString();

View File

@ -38,6 +38,11 @@ public class UpdateRoleEvent extends Event {
log.trace("\t\t\t\t\tNeue Admin: {}", group.getAdmins());
}
@Override
public String format() {
return "Mitgliedsrolle gesetzt: " + target + ": " + role + ".";
}
@Override
public String type() {
return EventType.UPDATEROLE.toString();

View File

@ -2,11 +2,11 @@ package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class UserAlreadyExistsException extends EventException {
public class UserExistsException extends EventException {
private static final long serialVersionUID = -8150634358760194625L;
public UserAlreadyExistsException(String info) {
public UserExistsException(String info) {
super(HttpStatus.INTERNAL_SERVER_ERROR, "User existiert bereits.", info);
}
}

View File

@ -10,7 +10,7 @@ import mops.gruppen2.domain.exception.GroupFullException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.exception.LastAdminException;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.exception.UserAlreadyExistsException;
import mops.gruppen2.domain.exception.UserExistsException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.model.group.wrapper.Body;
import mops.gruppen2.domain.model.group.wrapper.Description;
@ -19,6 +19,7 @@ import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.helper.CommonHelper;
import mops.gruppen2.domain.service.helper.SortHelper;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import javax.validation.Valid;
@ -56,19 +57,21 @@ public class Group {
private GroupMeta meta = GroupMeta.EMPTY();
private GroupOptions options = GroupOptions.DEFAULT();
//TODO: UI set + use for options
private final GroupOptions options = GroupOptions.DEFAULT();
// Inhalt
private Title title = Title.EMPTY();
private Description description = Description.EMPTY();
//TODO: Asciidoc description
private Body body;
// Integrationen
// Teilnehmer
private Map<String, Membership> memberships = new HashMap<>();
private final Map<String, Membership> memberships = new HashMap<>();
// ####################################### Members ###########################################
@ -98,7 +101,7 @@ public class Group {
return memberships.get(userid).getRole();
}
public void addMember(String target, User user) throws UserAlreadyExistsException, GroupFullException {
public void addMember(String target, User user) throws UserExistsException, GroupFullException {
ValidationHelper.throwIfMember(this, target);
ValidationHelper.throwIfGroupFull(this);
@ -215,6 +218,26 @@ public class Group {
return !parent.isEmpty();
}
public boolean hasMaterial() {
return options.isHasMaterialIntegration();
}
public boolean hasForums() {
return options.isHasForumsIntegration();
}
public boolean hasCalendar() {
return options.isHasTermineIntegration();
}
public boolean hasModules() {
return options.isHasModulesIntegration();
}
public boolean hasPortfolios() {
return options.isHasPortfolioIntegration();
}
// ######################################## Setters ##########################################
@ -255,14 +278,20 @@ public class Group {
this.limit = limit;
}
public void setParent(String exec, @Valid Parent parent) throws NoAccessException {
public void setParent(String exec, @Valid Parent parent) throws NoAccessException, BadArgumentException {
ValidationHelper.throwIfNoAdmin(this, exec);
if (parent.getValue().equals(groupid)) {
throw new BadArgumentException("Die Gruppe kann nicht zu sich selbst gehören!");
}
this.parent = parent;
}
public void setLink(String exec, @Valid Link link) throws NoAccessException {
ValidationHelper.throwIfNoAdmin(this, exec);
if (link.getValue().equals(groupid.toString())) {
throw new BadArgumentException("Link kann nicht der GruppenID entsprechen.");
}
this.link = link;
}
@ -289,7 +318,9 @@ public class Group {
}
groupid = null;
type = null;
// Wenn man alles null setzt hat der cache mehr arbeit, weil dieser erst nach der löschung
// geupdated wird und sich link und mitgliedschaften selber heraussuchen muss
/*type = null;
parent = null;
limit = null;
link = null;
@ -298,11 +329,11 @@ public class Group {
title = null;
description = null;
body = null;
memberships = null;
memberships = null;*/
}
public String format() {
return title + " " + description;
return title + " - " + description;
}
@Override
@ -319,4 +350,8 @@ public class Group {
public static Group EMPTY() {
return new Group();
}
public long getVersion() {
return meta.getVersion();
}
}

View File

@ -21,6 +21,9 @@ class GroupMeta {
if (this.version >= version) {
throw new IdMismatchException("Die Gruppe ist bereits auf einem neueren Stand.");
}
if (this.version + 1 != version) {
throw new IdMismatchException("Es fehlen vorherige Events.");
}
return new GroupMeta(version, creator, creationDate);
}

View File

@ -20,6 +20,8 @@ class GroupOptions {
boolean hasMaterialIntegration;
boolean hasTermineIntegration;
boolean hasPortfolioIntegration;
boolean hasForumsIntegration;
boolean hasModulesIntegration;
static GroupOptions DEFAULT() {
return new GroupOptions(true,
@ -31,6 +33,8 @@ class GroupOptions {
null,
true,
true,
true,
true,
true);
}
}

View File

@ -1,51 +0,0 @@
package mops.gruppen2.domain.model.group;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.List;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SortHelper {
/**
* Sortiert die übergebene Liste an Gruppen, sodass Veranstaltungen am Anfang der Liste sind.
*
* @param groups Die Liste von Gruppen die sortiert werden soll
*/
public static void sortByGroupType(List<Group> groups) {
groups.sort((Group g1, Group g2) -> {
if (g1.getType() == Type.LECTURE) {
return -1;
}
if (g2.getType() == Type.LECTURE) {
return 1;
}
if (g1.getType() == Type.PUBLIC) {
return -1;
}
if (g2.getType() == Type.PUBLIC) {
return 1;
}
return 0;
});
}
public static List<Membership> sortByMemberRole(List<Membership> memberships) {
memberships.sort((Membership m1, Membership m2) -> {
if (m1.getRole() == Role.ADMIN) {
return -1;
}
if (m2.getRole() == Role.ADMIN) {
return 1;
}
return 0;
});
return memberships;
}
}

View File

@ -12,6 +12,7 @@ import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
@Log4j2
@Value
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@AllArgsConstructor
public class User {

View File

@ -4,18 +4,24 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.UUID;
@Value
public class Link {
@NotNull
@Size(min = 36, max = 36)
@JsonProperty("value")
String value;
UUID value;
public Link(String value) {
this.value = UUID.fromString(value);
}
public static Link RANDOM() {
return new Link(UUID.randomUUID().toString());
}
public String getValue() {
return value.toString();
}
}

View File

@ -5,12 +5,16 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.exception.BadPayloadException;
import mops.gruppen2.domain.service.helper.JsonHelper;
import mops.gruppen2.domain.service.helper.CommonHelper;
import mops.gruppen2.domain.service.helper.FileHelper;
import mops.gruppen2.persistance.EventRepository;
import mops.gruppen2.persistance.dto.EventDTO;
import org.springframework.stereotype.Service;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Log4j2
@ -52,13 +56,13 @@ public class EventStoreService {
*/
private static EventDTO getDTOFromEvent(Event event) {
try {
String payload = JsonHelper.serializeEvent(event);
String payload = FileHelper.serializeEventJson(event);
return new EventDTO(null,
event.getGroupid().toString(),
event.getVersion(),
event.getExec(),
event.getTarget(),
event.type(),
Timestamp.valueOf(event.getDate()),
payload);
} catch (JsonProcessingException e) {
log.error("Event ({}) konnte nicht serialisiert werden!", event, e);
@ -81,7 +85,7 @@ public class EventStoreService {
private static Event getEventFromDTO(EventDTO dto) {
try {
return JsonHelper.deserializeEvent(dto.getEvent_payload());
return FileHelper.deserializeEventJson(dto.getEvent_payload());
} catch (JsonProcessingException e) {
log.error("Payload {} konnte nicht deserialisiert werden!", dto.getEvent_payload(), e);
throw new BadPayloadException(EventStoreService.class.toString());
@ -95,4 +99,32 @@ public class EventStoreService {
public List<Event> findAllEvents() {
return getEventsFromDTOs(eventStore.findAllEvents());
}
public List<Event> findGroupEvents(UUID groupId) {
return getEventsFromDTOs(eventStore.findGroupEvents(groupId.toString()));
}
public List<Event> findGroupEvents(List<UUID> ids) {
return ids.stream()
.map(id -> eventStore.findGroupEvents(id.toString()))
.map(EventStoreService::getEventsFromDTOs)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
public List<String> findGroupPayloads(UUID groupId) {
return eventStore.findGroupPayloads(groupId.toString());
}
public List<EventDTO> findGroupDTOs(UUID groupid) {
return eventStore.findGroupEvents(groupid.toString());
}
public List<UUID> findChangedGroups(long eventid) {
return CommonHelper.stringsToUUID(eventStore.findChangedGroupIds(eventid));
}
public long findMaxEventId() {
return eventStore.findMaxEventId();
}
}

View File

@ -24,12 +24,15 @@ import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.springframework.stereotype.Service;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Behandelt Aufgaben, welche sich auf eine Gruppe beziehen.
@ -94,9 +97,11 @@ public class GroupService {
* @param exec Ausführender User
*/
public void addUsersToGroup(Group group, String exec, List<User> newUsers) {
setLimit(group, exec, getAdjustedUserLimit(newUsers, group));
List<User> users = newUsers.stream().distinct().collect(Collectors.toUnmodifiableList());
newUsers.forEach(newUser -> addUserSilent(group, exec, newUser.getId(), newUser));
setLimit(group, exec, getAdjustedUserLimit(users, group));
users.forEach(newUser -> addUserSilent(group, exec, newUser.getId(), newUser));
}
/**
@ -123,6 +128,8 @@ public class GroupService {
* @throws EventException Falls der User nicht gefunden wird
*/
public void toggleMemberRole(Group group, String exec, String target) {
ValidationHelper.throwIfNoMember(group, target);
updateRole(group, exec, target, group.getRole(target).toggle());
}
@ -192,7 +199,7 @@ public class GroupService {
* Prüft, ob der Nutzer Admin ist und ob der Titel valide ist.
* Bei keiner Änderung wird nichts erzeugt.
*/
public void setTitle(Group group, String exec, Title title) {
public void setTitle(Group group, String exec, @Valid Title title) {
if (group.getTitle().equals(title.getValue())) {
return;
}
@ -205,7 +212,7 @@ public class GroupService {
* Prüft, ob der Nutzer Admin ist und ob die Beschreibung valide ist.
* Bei keiner Änderung wird nichts erzeugt.
*/
public void setDescription(Group group, String exec, Description description) {
public void setDescription(Group group, String exec, @Valid Description description) {
if (group.getDescription().equals(description.getValue())) {
return;
}
@ -231,7 +238,7 @@ public class GroupService {
* Prüft, ob der Nutzer Admin ist und ob das Limit valide ist.
* Bei keiner Änderung wird nichts erzeugt.
*/
public void setLimit(Group group, String exec, Limit userLimit) {
public void setLimit(Group group, String exec, @Valid Limit userLimit) {
if (userLimit.getValue() == group.getLimit()) {
return;
}
@ -247,7 +254,8 @@ public class GroupService {
applyAndSave(group, new SetParentEvent(group.getId(), exec, parent));
}
public void setLink(Group group, String exec, Link link) {
//TODO: UI Link regenerieren button
public void setLink(Group group, String exec, @Valid Link link) {
if (group.getLink().equals(link.getValue())) {
return;
}

View File

@ -4,11 +4,12 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.SortHelper;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.infrastructure.GroupCache;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ -30,25 +31,35 @@ public class SearchService {
*
* @throws EventException Projektionsfehler
*/
//TODO: search in lectures
public List<Group> search(String search, String principal) {
public List<Group> searchString(String search, String principal) {
List<Group> groups = new ArrayList<>();
groups.addAll(groupCache.publics());
groups.addAll(groupCache.lectures());
groups = removeUserGroups(groups, principal);
SortHelper.sortByGroupType(groups);
if (search.isEmpty()) {
return groups;
}
log.debug("Es wurde gesucht nach: {}", search);
return groups.stream()
.filter(group -> group.format().toLowerCase().contains(search.toLowerCase()))
.collect(Collectors.toList());
}
public List<Group> searchType(Type type, String principal) {
log.debug("Es wurde gesucht nach: {}", type);
if (type == Type.LECTURE) {
return removeUserGroups(groupCache.lectures(), principal);
}
if (type == Type.PUBLIC) {
return removeUserGroups(groupCache.publics(), principal);
}
return Collections.emptyList();
}
private static List<Group> removeUserGroups(List<Group> groups, String principal) {
return groups.stream()
.filter(group -> !group.isMember(principal))

View File

@ -3,13 +3,24 @@ package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.api.GroupRequestWrapper;
import mops.gruppen2.infrastructure.api.GroupWrapper;
import java.util.List;
import java.util.stream.Collectors;
//TODO: sinnvolles format
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class APIHelper {
/*public static GroupRequestWrapper wrap(long status, List<Group> groupList) {
return new GroupRequestWrapper(status, groupList);
}*/
public static GroupRequestWrapper wrap(long status, List<Group> groupList) {
return new GroupRequestWrapper(status, wrap(groupList));
}
public static List<GroupWrapper> wrap(List<Group> groups) {
return groups.stream()
.map(GroupWrapper::new)
.collect(Collectors.toUnmodifiableList());
}
}

View File

@ -4,7 +4,9 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@ -13,4 +15,10 @@ public final class CommonHelper {
public static boolean uuidIsEmpty(UUID uuid) {
return "00000000-0000-0000-0000-000000000000".equals(uuid.toString());
}
public static List<UUID> stringsToUUID(List<String> changedGroupIds) {
return changedGroupIds.stream()
.map(UUID::fromString)
.collect(Collectors.toUnmodifiableList());
}
}

View File

@ -1,48 +0,0 @@
package mops.gruppen2.domain.service.helper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.exception.WrongFileException;
import mops.gruppen2.domain.model.group.User;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CsvHelper {
public static List<User> readCsvFile(MultipartFile file) throws EventException {
if (file == null || file.isEmpty()) {
return Collections.emptyList();
}
try {
List<User> userList = read(file.getInputStream());
return userList.stream()
.distinct()
.collect(Collectors.toList()); //filter duplicates from list
} catch (IOException e) {
log.error("File konnte nicht gelesen werden!", e);
throw new WrongFileException(file.getOriginalFilename());
}
}
private static List<User> read(InputStream stream) throws IOException {
CsvMapper mapper = new CsvMapper();
CsvSchema schema = mapper.schemaFor(User.class).withHeader().withColumnReordering(true);
ObjectReader reader = mapper.readerFor(User.class).with(schema);
return reader.<User>readValues(stream).readAll();
}
}

View File

@ -0,0 +1,148 @@
package mops.gruppen2.domain.service.helper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.exception.GroupNotFoundException;
import mops.gruppen2.domain.exception.WrongFileException;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.persistance.dto.EventDTO;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class FileHelper {
// ######################################## CSV #############################################
public static List<User> readCsvFile(MultipartFile file) throws EventException {
if (file == null || file.isEmpty()) {
return Collections.emptyList();
}
try {
List<User> userList = readCsv(file.getInputStream());
return userList.stream()
.distinct()
.collect(Collectors.toList()); //filter duplicates from list
} catch (IOException e) {
log.error("File konnte nicht gelesen werden!", e);
throw new WrongFileException(file.getOriginalFilename());
}
}
private static List<User> readCsv(InputStream stream) throws IOException {
CsvMapper mapper = new CsvMapper();
CsvSchema schema = mapper.schemaFor(User.class).withHeader().withColumnReordering(true);
ObjectReader reader = mapper.readerFor(User.class).with(schema);
return reader.<User>readValues(stream).readAll();
}
public static String writeCsvUserList(List<User> members) {
StringBuilder builder = new StringBuilder();
builder.append("id,givenname,familyname,email\n");
members.forEach(user -> builder.append(user.getId())
.append(",")
.append(user.getGivenname())
.append(",")
.append(user.getFamilyname())
.append(",")
.append(user.getEmail())
.append("\n"));
return builder.toString();
}
// ########################################## JSON ###########################################
/**
* Übersetzt eine Java-Event-Repräsentation zu einem JSON-Event-Payload.
*
* @param event Java-Event-Repräsentation
*
* @return JSON-Event-Payload als String
*
* @throws JsonProcessingException Bei JSON Fehler
*/
public static String serializeEventJson(Event event) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
String payload = mapper.writeValueAsString(event);
log.trace(payload);
return payload;
}
/**
* Übersetzt eine JSON-Event-Payload zu einer Java-Event-Repräsentation.
*
* @param json JSON-Event-Payload als String
*
* @return Java-Event-Repräsentation
*
* @throws JsonProcessingException Bei JSON Fehler
*/
public static Event deserializeEventJson(String json) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
Event event = mapper.readValue(json, Event.class);
log.trace(event);
return event;
}
// ############################################### TXT #######################################
public static String payloadsToPlain(List<String> payloads) {
return payloads.stream()
.map(payload -> payload + "\n")
.reduce((String payloadA, String payloadB) -> payloadA + payloadB)
.orElseThrow(() -> new GroupNotFoundException("Keine Payloads gefunden."));
}
public static String eventDTOsToSql(List<EventDTO> dtos) {
StringBuilder builder = new StringBuilder();
builder.append("INSERT INTO event(group_id, group_version, exec_id, target_id, event_date, event_payload)\nVALUES\n");
dtos.forEach(dto -> builder.append("('")
.append(dto.getGroup_id())
.append("','")
.append(dto.getGroup_version())
.append("','")
.append(dto.getExec_id())
.append("','")
.append(dto.getTarget_id())
.append("','")
.append(dto.getEvent_date())
.append("','")
.append(dto.getEvent_payload())
.append("'),\n"));
builder.replace(builder.length() - 2, builder.length(), ";");
return builder.toString();
}
// ############################################### SQL #######################################
}

View File

@ -1,50 +0,0 @@
package mops.gruppen2.domain.service.helper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.event.Event;
/**
* Übersetzt JSON-Event-Payloads zu Java-Event-Repräsentationen und zurück.
*/
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class JsonHelper {
/**
* Übersetzt eine Java-Event-Repräsentation zu einem JSON-Event-Payload.
*
* @param event Java-Event-Repräsentation
*
* @return JSON-Event-Payload als String
*
* @throws JsonProcessingException Bei JSON Fehler
*/
public static String serializeEvent(Event event) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
String payload = mapper.writeValueAsString(event);
log.trace(payload);
return payload;
}
/**
* Übersetzt eine JSON-Event-Payload zu einer Java-Event-Repräsentation.
*
* @param json JSON-Event-Payload als String
*
* @return Java-Event-Repräsentation
*
* @throws JsonProcessingException Bei JSON Fehler
*/
public static Event deserializeEvent(String json) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
Event event = mapper.readValue(json, Event.class);
log.trace(event);
return event;
}
}

View File

@ -7,6 +7,9 @@ import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.GroupCache;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -19,6 +22,21 @@ import java.util.UUID;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ProjectionHelper {
public static List<Group> project(List<Event> events) {
Map<UUID, Group> groups = new HashMap<>();
if (events.isEmpty()) {
return Collections.emptyList();
}
log.trace(groups);
log.trace(events);
events.forEach(event -> event.apply(getOrCreateGroup(groups, event.getGroupid())));
return new ArrayList<>(groups.values());
}
public static void project(Map<UUID, Group> groups, List<Event> events, GroupCache cache) {
if (events.isEmpty()) {
return;

View File

@ -0,0 +1,27 @@
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.model.group.Membership;
import mops.gruppen2.domain.model.group.Role;
import java.util.List;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SortHelper {
public static List<Membership> sortByMemberRole(List<Membership> memberships) {
memberships.sort((Membership m1, Membership m2) -> {
if (m1.getRole() == Role.ADMIN) {
return -1;
}
if (m2.getRole() == Role.ADMIN) {
return 1;
}
return 0;
});
return memberships;
}
}

View File

@ -7,7 +7,7 @@ import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.GroupFullException;
import mops.gruppen2.domain.exception.LastAdminException;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.exception.UserAlreadyExistsException;
import mops.gruppen2.domain.exception.UserExistsException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
@ -39,10 +39,10 @@ public final class ValidationHelper {
// ######################################## THROW ############################################
public static void throwIfMember(Group group, String userid) throws UserAlreadyExistsException {
public static void throwIfMember(Group group, String userid) throws UserExistsException {
if (group.isMember(userid)) {
log.error("Benutzer {} ist schon in Gruppe {}", userid, group);
throw new UserAlreadyExistsException(userid);
throw new UserExistsException(userid);
}
}

View File

@ -3,6 +3,8 @@ package mops.gruppen2.infrastructure;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.GroupNotFoundException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.service.EventStoreService;
@ -17,7 +19,15 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Cached alle existierenden Gruppen und einige Beziehungen.
* Gruppen können nach Typ angefragt werden, nach ID, nach Link oder nach User.
* Der Cache wird von den Events aktualisiert.
* Beim Aufruf der init() Methode werden alle bisherigen Events projiziert und die Gruppen gespeichert.
* Die Komplette Anwendung verwendet eine Instanz des Caches.
*/
@Log4j2
@RequiredArgsConstructor
@Component
@ -28,7 +38,7 @@ public class GroupCache {
private final Map<UUID, Group> groups = new HashMap<>();
private final Map<String, Group> links = new HashMap<>();
private final Map<String, List<Group>> users = new HashMap<>();
private final Map<String, List<Group>> users = new HashMap<>(); // Wird vielleicht zu groß?
private final Map<Type, List<Group>> types = new EnumMap<>(Type.class);
@ -59,6 +69,14 @@ public class GroupCache {
return links.get(link);
}
public List<Group> groups() {
if (groups.isEmpty()) {
return Collections.emptyList();
}
return List.copyOf(groups.values());
}
public List<Group> userGroups(String userid) {
if (!users.containsKey(userid)) {
return Collections.emptyList();
@ -67,6 +85,24 @@ public class GroupCache {
return Collections.unmodifiableList(users.get(userid));
}
public List<Group> userLectures(String userid) {
return userGroups(userid).stream()
.filter(Group::isLecture)
.collect(Collectors.toUnmodifiableList());
}
public List<Group> userPublics(String userid) {
return userGroups(userid).stream()
.filter(Group::isPublic)
.collect(Collectors.toUnmodifiableList());
}
public List<Group> userPrivates(String userid) {
return userGroups(userid).stream()
.filter(Group::isPrivate)
.collect(Collectors.toUnmodifiableList());
}
public List<Group> publics() {
if (!types.containsKey(Type.PUBLIC)) {
return Collections.emptyList();
@ -96,6 +132,9 @@ public class GroupCache {
public void usersPut(String userid, Group group) {
if (!group.isMember(userid)) {
throw new UserNotFoundException("User ist kein Mitglied, Gruppe nicht gecached.");
}
if (!users.containsKey(userid)) {
users.put(userid, new ArrayList<>());
log.debug("Ein User wurde dem Cache hinzugefügt.");
@ -113,18 +152,37 @@ public class GroupCache {
}
public void groupsPut(UUID groupid, Group group) {
if (group.getId() != groupid) {
throw new IdMismatchException("ID passt nicht zu Gruppe, Gruppe nicht gecached.");
}
groups.put(groupid, group);
}
public void groupsRemove(Group group) {
groups.remove(group.getId());
public void groupsRemove(UUID groupid, Group group) {
if (!groups.containsKey(groupid)) {
return;
}
groups.remove(groupid);
links.remove(group.getLink());
group.getMembers().forEach(user -> users.get(user.getId()).removeIf(usergroup -> !usergroup.exists()));
types.get(group.getType()).removeIf(typegroup -> !typegroup.exists());
}
public void linksPut(String link, Group group) {
if (!link.equals(group.getLink())) {
throw new IdMismatchException("Link passt nicht zu Gruppe, Gruppe nicht gecached.");
}
links.put(link, group);
}
public void linksRemove(String link) {
if (!links.containsKey(link)) {
return;
}
links.remove(link);
}
@ -133,15 +191,18 @@ public class GroupCache {
types.put(type, new ArrayList<>());
log.debug("Ein Typ wurde dem Cache hinzugefügt.");
}
if (group.getType() != type) {
throw new IdMismatchException("Typ passt nicht zu Gruppe, Gruppe nicht gecached.");
}
types.get(type).add(group);
}
public void typesRemove(Group group) {
if (!types.containsKey(group.getType())) {
public void typesRemove(Type type, Group group) {
if (!types.containsKey(type)) {
return;
}
types.get(group.getType()).remove(group);
types.get(type).remove(group);
}
}

View File

@ -1,12 +1,14 @@
package mops.gruppen2.infrastructure.api;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* Kombiniert den Status und die Gruppenliste zur ausgabe über die API.
*/
@Getter
@AllArgsConstructor
public class GroupRequestWrapper {

View File

@ -1,4 +1,31 @@
package mops.gruppen2.infrastructure.api;
import lombok.Value;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.model.group.User;
import java.util.List;
import java.util.UUID;
@Value
public class GroupWrapper {
UUID groupid;
Type type;
UUID parent;
String title;
String description;
List<User> admins;
List<User> regulars;
public GroupWrapper(Group group) {
groupid = group.getId();
type = group.getType();
parent = group.getParent();
title = group.getTitle();
description = group.getDescription();
admins = group.getAdmins();
regulars = group.getRegulars();
}
}

View File

@ -1,13 +1,27 @@
package mops.gruppen2.infrastructure.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.domain.service.helper.APIHelper;
import mops.gruppen2.domain.service.helper.ProjectionHelper;
import mops.gruppen2.infrastructure.GroupCache;
import mops.gruppen2.infrastructure.api.GroupRequestWrapper;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Api zum Datenabgleich.
*/
@ -18,8 +32,7 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/gruppen2/api")
public class APIController {
//TODO: redo api
private final GroupCache cache;
private final EventStoreService eventStoreService;
/**
@ -28,38 +41,43 @@ public class APIController {
*
* @param eventId Die Event-ID, welche der Anfragesteller beim letzten Aufruf erhalten hat
*/
/*@GetMapping("/update/{id}")
//TODO: sollte den cache benutzen, am besten wäre eine groupversion, welche der eventid
//TODO: entspricht, dann kann man leicht alle geänderten gruppen finden
@GetMapping("/update/{id}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt veränderte Gruppen zurück")
public GroupRequestWrapper getApiUpdate(@ApiParam("Letzte gespeicherte EventId des Anfragestellers")
@PathVariable("id") long eventId) {
return APIHelper.wrap(eventStoreService.findMaxEventId(),
projectionHelper.projectChangedGroups(eventId));
}*/
ProjectionHelper.project(eventStoreService.findGroupEvents(eventStoreService.findChangedGroups(eventId))));
}
/**
* Gibt die Gruppen-IDs von Gruppen, in welchen der übergebene Nutzer teilnimmt, zurück.
*/
/*@GetMapping("/usergroups/{id}")
@GetMapping("/usergroups/{id}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt Gruppen zurück, in welchen ein Nutzer teilnimmt")
public List<String> getApiUserGroups(@ApiParam("Nutzer-Id")
@PathVariable("id") String userId) {
return CommonHelper.uuidsToString(eventStoreService.findExistingUserGroups(userId));
}*/
return cache.userGroups(userId).stream()
.map(Group::getId)
.map(UUID::toString)
.collect(Collectors.toUnmodifiableList());
}
/**
* Konstruiert eine einzelne, vollständige Gruppe.
*/
/*@GetMapping("/group/{id}")
@GetMapping("/group/{id}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt die Gruppe mit der als Parameter mitgegebenden groupId zurück")
public Group getApiGroup(@ApiParam("Gruppen-Id der gefordeten Gruppe")
@PathVariable("id") String groupId) {
return projectionHelper.projectGroupById(UUID.fromString(groupId));
}*/
return cache.group(UUID.fromString(groupId));
}
}

View File

@ -11,7 +11,7 @@ import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.GroupService;
import mops.gruppen2.domain.service.helper.CsvHelper;
import mops.gruppen2.domain.service.helper.FileHelper;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
@ -67,7 +67,7 @@ public class GroupCreationController {
// ROLE_studentin kann kein CSV importieren
if (token.getAccount().getRoles().contains("orga")) {
groupService.addUsersToGroup(group, principal, CsvHelper.readCsvFile(file));
groupService.addUsersToGroup(group, principal, FileHelper.readCsvFile(file));
}
return "redirect:/gruppen2/details/" + group.getId();

View File

@ -8,11 +8,13 @@ import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.domain.service.GroupService;
import mops.gruppen2.domain.service.helper.CsvHelper;
import mops.gruppen2.domain.service.helper.FileHelper;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -24,7 +26,9 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.UUID;
@SuppressWarnings("SameReturnValue")
@ -37,6 +41,7 @@ public class GroupDetailsController {
private final GroupCache groupCache;
private final GroupService groupService;
private final EventStoreService eventStoreService;
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/details/{id}")
@ -94,6 +99,77 @@ public class GroupDetailsController {
return "redirect:/gruppen2";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("details/{id}/history")
public String getDetailsHistory(KeycloakAuthenticationToken token,
Model model,
@PathVariable("id") String groupId) {
model.addAttribute("events",
eventStoreService.findGroupEvents(UUID.fromString(groupId)));
return "log";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping(value = "details/{id}/export/history/plain", produces = "text/plain;charset=UTF-8")
public void getDetailsExportHistoryPlain(HttpServletResponse response,
@PathVariable("id") String groupId) {
String filename = "eventlog-" + groupId + ".txt";
response.setContentType("text/txt;charset=UTF-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"");
try {
response.getWriter()
.write(FileHelper.payloadsToPlain(
eventStoreService.findGroupPayloads(UUID.fromString(groupId))));
} catch (IOException e) {
log.error("Payloads konnten nicht geschrieben werden.", e);
}
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping(value = "details/{id}/export/history/sql", produces = "application/sql;charset=UTF-8")
public void getDetailsExportHistorySql(HttpServletResponse response,
@PathVariable("id") String groupId) {
String filename = "data.sql";
response.setContentType("application/sql;charset=UTF-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"");
try {
response.getWriter()
.write(FileHelper.eventDTOsToSql(
eventStoreService.findGroupDTOs(UUID.fromString(groupId))));
} catch (IOException e) {
log.error("Payloads konnten nicht geschrieben werden.", e);
}
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping(value = "details/{id}/export/members", produces = "text/csv;charset=UTF-8")
public void getDetailsExportMembers(HttpServletResponse response,
@PathVariable("id") String groupId) {
String filename = "teilnehmer-" + groupId + ".csv";
response.setContentType("text/csv;charset=UTF-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"");
try {
response.getWriter()
.print(FileHelper.writeCsvUserList(groupCache.group(UUID.fromString(groupId)).getMembers()));
} catch (IOException e) {
log.error("Teilnehmerliste konnte nicht geschrieben werden.", e);
}
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/details/{id}/edit")
public String getDetailsEdit(KeycloakAuthenticationToken token,
@ -127,8 +203,6 @@ public class GroupDetailsController {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
System.out.println(group);
groupService.setTitle(group, principal, title);
groupService.setDescription(group, principal, description);
@ -157,7 +231,7 @@ public class GroupDetailsController {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
groupService.addUsersToGroup(group, principal, CsvHelper.readCsvFile(file));
groupService.addUsersToGroup(group, principal, FileHelper.readCsvFile(file));
return "redirect:/gruppen2/details/" + groupId + "/edit";
}
@ -172,6 +246,9 @@ public class GroupDetailsController {
Group group = groupCache.group(UUID.fromString(groupId));
ValidationHelper.throwIfNoAdmin(group, principal);
if (target.equals(principal)) {
ValidationHelper.throwIfLastAdmin(group, principal);
}
groupService.toggleMemberRole(group, principal, target);

View File

@ -33,7 +33,10 @@ public class GruppenfindungController {
@GetMapping("/gruppen2")
public String getIndexPage(KeycloakAuthenticationToken token,
Model model) {
model.addAttribute("groups", groupCache.userGroups(token.getName()));
model.addAttribute("lectures", groupCache.userLectures(token.getName()));
model.addAttribute("publics", groupCache.userPublics(token.getName()));
model.addAttribute("privates", groupCache.userPrivates(token.getName()));
return "index";
}

View File

@ -41,13 +41,40 @@ public class SearchAndInviteController {
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/search")
public String postSearch(KeycloakAuthenticationToken token,
Model model,
@RequestParam("string") String search) {
@PostMapping("/search/string")
public String postSearchString(KeycloakAuthenticationToken token,
Model model,
@RequestParam("string") String search) {
String principal = token.getName();
List<Group> groups = searchService.search(search, principal);
List<Group> groups = searchService.searchString(search, principal);
model.addAttribute("groups", groups);
return "search";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/search/all")
public String getSearchAll(KeycloakAuthenticationToken token,
Model model) {
String principal = token.getName();
List<Group> groups = searchService.searchString("", principal);
model.addAttribute("groups", groups);
return "search";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/search/type/{type}")
public String getSearchType(KeycloakAuthenticationToken token,
Model model,
@PathVariable("type") Type type) {
String principal = token.getName();
List<Group> groups = searchService.searchType(type, principal);
model.addAttribute("groups", groups);

View File

@ -3,6 +3,7 @@ package mops.gruppen2.persistance;
import mops.gruppen2.persistance.dto.EventDTO;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@ -16,4 +17,16 @@ public interface EventRepository extends CrudRepository<EventDTO, Long> {
@Query("SELECT * FROM event")
List<EventDTO> findAllEvents();
@Query("SELECT * FROM event WHERE group_id = :groupid")
List<EventDTO> findGroupEvents(@Param("groupid") String groupId);
@Query("SELECT event_payload FROM event WHERE group_id = :groupid")
List<String> findGroupPayloads(@Param("groupid") String groupId);
@Query("SELECT MAX(event_id) FROM event")
long findMaxEventId();
@Query("SELECT DISTINCT group_id FROM event WHERE event_id > :eventid")
List<String> findChangedGroupIds(@Param("eventid") long eventid);
}

View File

@ -5,6 +5,8 @@ import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import java.sql.Timestamp;
@Table("event")
@Getter
@AllArgsConstructor
@ -19,6 +21,6 @@ public class EventDTO {
String exec_id;
String target_id;
String event_type;
Timestamp event_date;
String event_payload;
}

View File

@ -11,12 +11,12 @@ spring.profiles.active = dev
#keycloak.use-resource-role-mappings = true
#keycloak.autodetect-bearer-only = true
#keycloak.confidential-port = 443
keycloak.auth-server-url = https://gruppenkeycloak.herokuapp.com/auth
hhu_keycloak.token-uri = https://gruppenkeycloak.herokuapp.com/auth/realms/master/protocol/openid-connect/token
keycloak.auth-server-url = https://churl-keycloak.herokuapp.com/auth
hhu_keycloak.token-uri = https://churl-keycloak.herokuapp.com/auth/realms/gruppen/protocol/openid-connect/token
keycloak.principal-attribute = preferred_username
keycloak.realm = master
keycloak.realm = gruppen
keycloak.resource = gruppen-app
keycloak.credentials.secret = f73354fd-614e-4e24-b801-a8d0f4abf531
keycloak.credentials.secret = 2e2e5770-c454-4d31-be99-9d8c34c93089
keycloak.verify-token-audience = true
keycloak.use-resource-role-mappings = true
keycloak.autodetect-bearer-only = true

View File

@ -0,0 +1,27 @@
INSERT INTO event(group_id, group_version, exec_id, target_id, event_date, event_payload)
VALUES ('e65dd5f1-b252-4512-8db4-0407b31d199f', '1', 'orga', 'null', '2020-04-17 18:52:01.259555',
'{"class":"CREATEGROUP","date":[2020,4,17,18,52,1,259555000],"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":1,"exec":"orga","target":null}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '2', 'orga', 'orga', '2020-04-17 18:52:01.291448',
'{"class":"ADDMEMBER","user":{"id":"orga","givenname":"Thomas","familyname":"Organisator","email":"orga@hhu.de"},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":2,"exec":"orga","target":"orga","date":[2020,4,17,18,52,1,291448000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '3', 'orga', 'orga', '2020-04-17 18:52:01.301972',
'{"class":"UPDATEROLE","role":"ADMIN","groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":3,"exec":"orga","target":"orga","date":[2020,4,17,18,52,1,301972000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '4', 'orga', 'null', '2020-04-17 18:52:01.3053',
'{"class":"SETLIMIT","limit":{"value":1000},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":4,"exec":"orga","target":null,"date":[2020,4,17,18,52,1,305300000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '5', 'orga', 'null', '2020-04-17 18:52:01.310406',
'{"class":"SETTYPE","type":"LECTURE","groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":5,"exec":"orga","target":null,"date":[2020,4,17,18,52,1,310406000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '6', 'orga', 'null', '2020-04-17 18:52:01.313421',
'{"class":"SETPARENT","parent":{"id":"00000000-0000-0000-0000-000000000000"},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":6,"exec":"orga","target":null,"date":[2020,4,17,18,52,1,313421000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '7', 'orga', 'null', '2020-04-17 18:52:01.317931',
'{"class":"SETTITLE","title":{"value":"Programmierung SS2020"},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":7,"exec":"orga","target":null,"date":[2020,4,17,18,52,1,317931000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '8', 'orga', 'null', '2020-04-17 18:52:01.320693',
'{"class":"SETDESCRIPTION","desc":{"value":"Einführung in die objektorientierte Programmierung mit Java."},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":8,"exec":"orga","target":null,"date":[2020,4,17,18,52,1,320693000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '9', 'orga', 'A5ggd', '2020-04-17 18:52:01.330879',
'{"class":"ADDMEMBER","user":{"id":"A5ggd","givenname":"peter","familyname":"pan","email":"peter@pan.com"},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":9,"exec":"orga","target":"A5ggd","date":[2020,4,17,18,52,1,330879000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '10', 'orga', 'Affs', '2020-04-17 18:52:01.333756',
'{"class":"ADDMEMBER","user":{"id":"Affs","givenname":"olaf","familyname":"pomodoro","email":"ol@f-99.de"},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":10,"exec":"orga","target":"Affs","date":[2020,4,17,18,52,1,333756000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '11', 'orga', '55fdd', '2020-04-17 18:52:01.336206',
'{"class":"ADDMEMBER","user":{"id":"55fdd","givenname":"dieter","familyname":"niemöller","email":"pfarrer@erde.de"},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":11,"exec":"orga","target":"55fdd","date":[2020,4,17,18,52,1,336206000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '12', 'orga', '22ööl', '2020-04-17 18:52:01.338582',
'{"class":"ADDMEMBER","user":{"id":"22ööl","givenname":"thomas","familyname":"müller","email":"thot@scheisse.de"},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":12,"exec":"orga","target":"22ööl","date":[2020,4,17,18,52,1,338582000]}'),
('e65dd5f1-b252-4512-8db4-0407b31d199f', '13', 'orga', 'tdsd8', '2020-04-17 18:52:01.341216',
'{"class":"ADDMEMBER","user":{"id":"tdsd8","givenname":"tobidignouserandingdong","familyname":"abraham","email":"g@gmail.mail"},"groupid":"e65dd5f1-b252-4512-8db4-0407b31d199f","version":13,"exec":"orga","target":"tdsd8","date":[2020,4,17,18,52,1,341216000]}');

View File

@ -7,6 +7,6 @@ CREATE TABLE event
group_version INT NOT NULL,
exec_id VARCHAR(50) NOT NULL,
target_id VARCHAR(50),
event_type VARCHAR(32) NOT NULL,
event_date DATETIME NOT NULL,
event_payload VARCHAR(2500) NOT NULL
);

View File

@ -3,34 +3,15 @@
margin-bottom: 15px;
}
/*Buttons*/
.edit {
font-size: 20px;
}
.btn-bar {
align-self: start;
}
/*Badges*/
.badge-pill {
align-self: start;
margin-right: 10px;
}
.members .badge {
align-self: start;
margin-top: 2px;
}
/*Member List*/
.members {
overflow-y: auto;
overflow-x: hidden;
max-height: calc(100vh - 250px);
max-width: 100%;
}
.members li span {
li span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}

View File

@ -1,19 +1,16 @@
/*Grouplist Hover Effect*/
.content {
padding-right: 20px;
background: #e6f4ff;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
}
.content:hover {
padding-right: 10px;
background: #d4ecff;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .25);
}
.content:hover .content-heading {
padding-bottom: 10px;
padding-left: 10px;
}
.content-text-in {
@ -26,6 +23,5 @@
/*Badges*/
.badge-pill {
margin-left: 10px;
align-self: start;
margin-right: 15px;
}

View File

@ -23,5 +23,5 @@
.badge-pill {
align-self: start;
margin-top: 2px;
margin-right: 5px;
margin-right: 15px;
}

View File

@ -16,6 +16,9 @@
margin-bottom: 15px;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .25);
transition: 100ms;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
}
/*Content Heading*/
@ -33,6 +36,12 @@
white-space: nowrap;
}
h1, h3 {
cursor: default;
overflow: hidden;
text-overflow: ellipsis;
}
/*Content Paragraph*/
.content-text {
margin-bottom: 15px;
@ -65,10 +74,19 @@
color: #4fa4ff;
}
/*Button*/
.btn {
width: 250px;
}
.btn-spacer {
}
/*Badges*/
.badge-pill {
cursor: default;
color: whitesmoke;
align-self: start;
}
.lecture {
@ -87,18 +105,31 @@
background: var(--warning);
}
/*Grid System*/
.row {
margin-left: 0;
margin-right: 0;
/*Input*/
.input-group {
width: auto;
}
.col {
padding-left: 0;
padding-right: 0;
.input-group-append {
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
}
/*Miscellanous*/
.def-cursor {
cursor: default;
.input-group-append {
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
}
.input-group-append span {
overflow: hidden;
text-overflow: ellipsis;
font-family: monospace;
}
.input-group-append span {
overflow: hidden;
text-overflow: ellipsis;
font-family: monospace;
}

View File

@ -9,37 +9,35 @@
<body>
<main th:fragment="bodycontent">
<div class="container">
<h1>Neue Gruppe</h1>
<h1 class="def-cursor">Neue Gruppe</h1>
<form enctype="multipart/form-data" method="post" th:action="@{/gruppen2/create}">
<div class="content px-1 px-sm-3 mx-n2 mx-sm-0">
<h3>Eigenschaften:</h3>
<form enctype="multipart/form-data" method="post" th:action="@{/gruppen2/create}">
<div class="content">
<h3 class="def-cursor">Eigenschaften:</h3>
<!--Titel + Beschreibung-->
<div class="content-text" th:insert="~{fragments/forms :: meta}"></div>
<!--Titel + Beschreibung-->
<div class="content-text" th:insert="~{fragments/forms :: meta}"></div>
<!--TODO: Enter AsciiDoc Description-->
<!--TODO: Enter AsciiDoc Description-->
<div class="content-text-in">
<!--Gruppentyp-->
<div th:insert="~{fragments/forms :: grouptype}"></div>
<div class="content-text-in">
<!--Gruppentyp-->
<div th:replace="~{fragments/forms :: grouptype}"></div>
<!--Benutzerlimit-->
<div th:replace="~{fragments/forms :: userlimit}"></div>
</div>
<!--CSV Import-->
<div class="content-text mb-0" th:insert="~{fragments/forms :: csvimport}"></div>
<!--Benutzerlimit-->
<div class="mt-2" th:insert="~{fragments/forms :: userlimit}"></div>
</div>
<div class="content">
<!--Submit-->
<button class="btn btn-primary btn-block" type="submit">Gruppe Erstellen</button>
</div>
</form>
</div>
<!--CSV Import-->
<div class="content-text mb-0" th:insert="~{fragments/forms :: csvimport}"></div>
</div>
<div class="content d-flex flex-row flex-wrap px-1 px-sm-3">
<!--Submit-->
<button class="btn btn-primary btn-block ml-auto" type="submit">Gruppe Erstellen
</button>
</div>
</form>
</main>
</body>

View File

@ -9,60 +9,198 @@
<body>
<main th:fragment="bodycontent">
<div class="container-fluid">
<h1 th:text="${group.getTitle()}"></h1>
<h1 class="def-cursor" th:text="${group.getTitle()}"></h1>
<div class="d-flex flex-row flex-wrap mr-n4 mt-n2">
<!--Gruppendetails-->
<div class="flex-grow-1 mr-4 mt-2" style="flex-basis: 65%;">
<div class="row">
<!--Gruppendetails-->
<div class="col-9 px-0">
<div class="content py-2 px-1 px-sm-3 mx-n2 mx-sm-0"
th:insert="~{fragments/groups :: groupcontent}"></div>
<div class="content" th:insert="~{fragments/groups :: groupcontent}"></div>
<div class="content">
<!--Button-Bar-->
<div class="row">
<a class="btn btn-primary" href="/gruppen2">Fertig</a>
<!--Dummy Integrationen, der Inhalt kommt natürlich aus der Gruppe bzw. von anderen Systemen-->
<div class="d-flex flex-row flex-wrap mr-n2 mt-n2">
<!--Materialsammlung-->
<div th:if="${group.hasMaterial()}" class="content flex-grow-1 mr-2 mt-2 py-2 px-1 px-sm-3">
<div class="content-heading">
<span>Gruppenmaterialien</span>
</div>
<!--Spacer-->
<span class="col"></span>
<div class="content-text-in">
<h3>Neue Materialien:</h3>
<ul>
<li><a href="/">Blatt 3.pdf</a></li>
<li><a href="/">Vorlesung 5.pdf</a></li>
<li><a href="/">Wikipedia.de/blabla</a></li>
</ul>
<h3>Angepinnt:</h3>
<ul>
<li><a href="/">Sicherheitsregeln.pdf</a></li>
<li><a href="/">Bewertungskriterien.pdf</a></li>
</ul>
</div>
<div class="content-text d-flex flex-row flex-wrap mt-n2 mr-n2">
<a class="btn btn-success flex-grow-1 mt-2 mr-2" href="/">Neuer
Upload.</a>
<a class="btn btn-info ml-auto flex-grow-1 mt-2 mr-2" href="/">
Zur Materialsammlung.</a>
</div>
</div>
<!--Foren-->
<div th:if="${group.hasForums()}" class="content flex-grow-1 mr-2 mt-2 py-2 px-1 px-sm-3">
<div class="content-heading">
<span>Forum</span>
</div>
<div class="content-text-in">
<h3>Letzte Aktivität:</h3>
<ul>
<li><a href="/">"Re: Ey schick lösung"</a></li>
<li><a href="/">"Re: Aufgabe 2"</a></li>
<li><a href="/">"Neu: Ankündigung"</a></li>
</ul>
</div>
<div class="content-text d-flex flex-row flex-wrap mt-n2 mr-n2">
<a class="btn btn-success flex-grow-1 mt-2 mr-2" href="/">
Frage stellen.</a>
<a class="btn btn-info flex-grow-1 ml-auto mt-2 mr-2" href="/">
Zum Forum.</a>
</div>
</div>
<!--Termine-->
<div th:if="${group.hasCalendar()}" class="content flex-grow-1 mr-2 mt-2 py-2 px-1 px-sm-3">
<div class="content-heading">
<span>Gruppentermine</span>
</div>
<div class="content-text-in">
<h3>Nächste Fristen:</h3>
<ul>
<li><a href="/">"Abgabe Übungsblatt 3"</a></li>
<li><a href="/">"Feedback VL 5"</a></li>
<li><a href="/">"Übungsgruppe belegen"</a></li>
</ul>
</div>
<div class="content-text d-flex flex-row flex-wrap mt-n2 mr-n2">
<a class="btn btn-success flex-grow-1 mt-2 mr-2" href="/">
Abstimmung starten.</a>
<a class="btn btn-info flex-grow-1 ml-auto mt-2 mr-2" href="/">
Zur Terminfindung.</a>
</div>
</div>
<!--Portfolios-->
<div th:if="${group.hasPortfolios()}" class="content flex-grow-1 mr-2 mt-2 py-2 px-1 px-sm-3">
<div class="content-heading">
<span>Gruppenportfolio</span>
</div>
<div class="content-text-in">
<h3>Neueste Einträge:</h3>
<ul>
<li><a href="/">"Praktikum Tag 5"</a></li>
<li><a href="/">"Praktikum Tag 4"</a></li>
<li><a href="/">"Nacharbeit Tag 3"</a></li>
</ul>
</div>
<div class="content-text d-flex flex-row flex-wrap mt-n2 mr-n2">
<a class="btn btn-success flex-grow-1 mt-2 mr-2" href="/">
Neue Seite.</a>
<a class="btn btn-info flex-grow-1 ml-auto mt-2 mr-2" href="/">
Zum Portfolio.</a>
</div>
</div>
<!--Modulhandbuch-->
<div th:if="${group.hasModules()}" class="content flex-grow-1 mr-2 mt-2 py-2 px-1 px-sm-3">
<div class="content-heading">
<span>Modulhandbuch</span>
</div>
<div class="content-text-in">
<h3>Veranstaltungsinfo:</h3>
<p>
Dieses Modul vermittelt grundlegende Programmierkenntnisse in einer
objektorientierten Programmiersprache.
Darüber hinaus werden einführend Aspekte von Algorithmen und
Datenstrukturen behandelt.
Es wird keine Programmiererfahrung vorausgesetzt.
</p>
<ul>
<li>Grundlegende Begriffe der Informatik</li>
<li>Primitive Datentypen und Variablen</li>
<li>Kontrollstrukturen</li>
<li>Eigene Datentypen (Klassen) und Arrays</li>
<li>Programmstrukturen im Speicher (Heap, Stack)</li>
<li>Konzepte der Objektorientierung (Polymorphie, Schnittstellen)
</li>
<li>Rekursion</li>
<li>Fehlerbehandlung</li>
<li>Dynamische Datenstrukturen (Listen, Binärbäume, Hashing)</li>
<li>Suchen und Sortieren (ausgewählte Algorithmen, u.a. binäre
Suche, BubbleSort, QuickSort)
</li>
<li>Datenströme (Standard-Eingabe und -Ausgabe, einfache 2D-Grafik,
Dateien)
</li>
</ul>
</div>
<div class="content-text d-flex flex-row flex-wrap mt-n2 mr-n2">
<a class="btn btn-info flex-grow-1 ml-auto mt-2 mr-2" href="/">
Zum Modulhandbuch.</a>
</div>
</div>
</div>
<div class="content py-2 px-1 px-sm-3">
<!--Button-Bar-->
<div class="d-flex flex-row flex-wrap mr-n2 mt-n2">
<a class="btn btn-primary flex-grow-1 mr-2 mt-2" href="/gruppen2">Fertig</a>
<div class="ml-auto mr-2 mt-2 flex-grow-1 btn-spacer ml-auto">
<form method="post" th:action="@{/gruppen2/details/{id}/leave(id=${group.getId()})}">
<button class="btn btn-danger btn-bar" type="submit">Gruppe verlassen
<button class="btn btn-danger w-100" type="submit">
Gruppe verlassen
</button>
</form>
</div>
</div>
</div>
</div>
<!--Teilnehmerliste-->
<div class="col-3 def-cursor">
<!--Anzahl Text-->
<div class="mb-2">
<span>Teilnehmer: </span>
<span th:text="${group.size() + ' von ' + group.getLimit()}"></span>
</div>
<!--Bearbeiten-Button-->
<div class="mb-2" th:if="${group.isAdmin(principal.getId())}">
<form method="get"
th:action="@{/gruppen2/details/{id}/edit(id=${group.getId()})}">
<button class="btn btn-secondary btn-block">Gruppe verwalten</button>
</form>
</div>
<!--Liste-->
<div class="members">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between"
th:each="member : ${group.getMembers()}">
<span th:text="${member.format()}"></span>
<span th:replace="~{fragments/groups :: userbadges}"></span>
</li>
</ul>
</div>
<!--Teilnehmerliste-->
<div class="def-cursor flex-grow-1 mr-4 mt-2 mw-100 overflow-hidden py-2 px-1 px-sm-3">
<!--Anzahl Text-->
<div class="mb-2">
<span>Teilnehmer: </span>
<span th:text="${group.size() + ' von ' + group.getLimit()}"></span>
</div>
<!--Bearbeiten-Button-->
<div class="mb-2" th:if="${group.isAdmin(principal.getId())}">
<form method="get"
th:action="@{/gruppen2/details/{id}/edit(id=${group.getId()})}">
<button class="btn btn-secondary btn-block">Gruppe verwalten</button>
</form>
</div>
<!--Liste-->
<div class="members">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between"
th:each="member : ${group.getMembers()}">
<span th:text="${member.format()}"></span>
<span th:replace="~{fragments/groups :: userbadges}"></span>
</li>
</ul>
</div>
</div>
</div>
</main>

View File

@ -9,113 +9,148 @@
<body>
<main th:fragment="bodycontent">
<div class="container-fluid">
<h1 class="def-cursor" th:text="${group.getTitle()}"></h1>
<h1 th:text="${group.getTitle()}"></h1>
<!--Fertig oder löschen-->
<div class="content">
<div class="row">
<form method="get" th:action="@{/gruppen2/details/{id}(id=${group.getId()})}">
<button class="btn btn-primary">Fertig</button>
</form>
<!--Spacer-->
<span class="col"></span>
<!--Fertig oder löschen-->
<div class="content py-2 px-1 px-sm-3 mx-n2 mx-sm-0">
<div class="d-flex flex-row flex-wrap mt-n2 mr-n2">
<a class="btn btn-primary flex-grow-1 mt-2 mr-2" style="max-width: 250px;"
th:href="@{/gruppen2/details/{id}(id=${group.getId()})}">Fertig</a>
<div class="mt-2 mr-2 flex-grow-1 ml-auto btn-spacer">
<form method="post" th:action="@{/gruppen2/details/{id}/edit/destroy(id=${group.getId()})}">
<button class="btn btn-danger btn-bar" type="submit">Gruppe löschen
<button class="btn btn-danger w-100" type="submit">Gruppe löschen
</button>
</form>
</div>
</div>
</div>
<!--Invite Link-->
<div class="content input-group">
<div class="input-group-prepend">
<span class="input-group-text">Einladungslink:</span>
</div>
<input class="form-control" id="linkview" readonly th:value="${link}" type="text">
<div class="input-group-append">
<button type="button" class="btn btn-secondary"
onclick="copyLink()">Link kopieren
</button>
</div>
<!--Invite Link-->
<div class="content input-group py-2 px-1 px-sm-3 mx-n2 mx-sm-0">
<div class="input-group-prepend">
<span class="input-group-text">Einladungslink:</span>
</div>
<!--Meta-->
<div class="content">
<div class="content-heading">
<span>Eigenschaften</span>
</div>
<!--Beschreibung + Titel-->
<div class="content-text">
<form th:action="@{/gruppen2/details/{id}/edit/meta(id=${group.getId()})}"
method="post">
<div th:replace="~{fragments/forms :: meta}"></div>
<div class="row">
<span class="col"></span>
<button type="submit" class="btn btn-secondary mt-2">Speichern</button>
</div>
</form>
</div>
<!--Userlimit-->
<div class="content-text-in">
<form th:action="@{/gruppen2/details/{id}/edit/userlimit(id=${group.getId()})}"
method="post">
<div th:replace="~{fragments/forms :: userlimit}"></div>
<div class="row">
<span class="col"></span>
<button type="submit" class="btn btn-secondary mt-2">Speichern</button>
</div>
</form>
</div>
<!--CSV Import-->
<div class="content-text mb-0">
<form th:action="@{/gruppen2/details/{id}/edit/csv(id=${group.getId()})}"
th:if="${account.getRoles().contains('orga')}"
enctype="multipart/form-data" method="post">
<div th:replace="~{fragments/forms :: csvimport}"></div>
<div class="row">
<span class="col"></span>
<button type="submit" class="btn btn-secondary mt-2">Speichern</button>
</div>
</form>
</div>
</div>
<!--Teilnehmerliste-->
<div class="content members">
<div class="content-heading">
<span>Teilnehmer</span>
</div>
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between" th:each="member : ${group.getMembers()}">
<div>
<span th:text="${member.format()}"></span>
<span th:replace="~{fragments/groups :: userbadges}"></span>
</div>
<div class="d-flex">
<form th:action="@{/gruppen2/details/{id}/edit/delete/{userid}(id=${group.getId()}, userid=${member.getId()})}"
th:unless="${member.getId() == account.getName()}"
method="post">
<button type="submit" class="btn btn-danger mr-2">Entfernen</button>
</form>
<form th:action="@{/gruppen2/details/{id}/edit/role/{userid}(id=${group.getId()}, userid=${member.getId()})}"
method="post">
<button type="submit" class="btn btn-warning">Rolle ändern</button>
</form>
</div>
</li>
</ul>
<input class="form-control" id="linkview" readonly th:value="${link}" type="text">
<div class="input-group-append">
<button type="button" class="btn btn-secondary" onclick="copyLink()">Link kopieren
</button>
</div>
</div>
<!--Meta-->
<div class="content py-2 px-1 px-sm-3 mx-n2 mx-sm-0">
<div class="content-heading">
<span>Eigenschaften</span>
</div>
<!--Beschreibung + Titel-->
<div class="content-text">
<form th:action="@{/gruppen2/details/{id}/edit/meta(id=${group.getId()})}"
method="post">
<div th:replace="~{fragments/forms :: meta}"></div>
<div class="d-flex flex-row flex-wrap">
<button type="submit" class="btn btn-secondary mt-2 ml-auto">Speichern
</button>
</div>
</form>
</div>
<!--Userlimit-->
<div class="content-text-in px-1 px-sm-3">
<form th:action="@{/gruppen2/details/{id}/edit/userlimit(id=${group.getId()})}"
method="post">
<div th:replace="~{fragments/forms :: userlimit}"></div>
<div class="d-flex flex-row flex-wrap">
<button type="submit" class="btn btn-secondary mt-2 ml-auto">Speichern
</button>
</div>
</form>
</div>
<!--CSV Import-->
<div class="content-text mb-0">
<form th:action="@{/gruppen2/details/{id}/edit/csv(id=${group.getId()})}"
th:if="${account.getRoles().contains('orga')}"
enctype="multipart/form-data" method="post">
<div th:replace="~{fragments/forms :: csvimport}"></div>
<div class="d-flex flex-row flex-wrap">
<button type="submit" class="btn btn-secondary mt-2 ml-auto">Speichern
</button>
</div>
</form>
</div>
</div>
<!--Export + Log-->
<div class="content py-2 px-1 px-sm-3 mx-n2 mx-sm-0">
<div class="content-heading">
<span>Event-Historie</span>
</div>
<div class="d-flex flex-row flex-wrap mt-n2 mr-n2">
<a class="btn btn-primary mt-2 mr-2 flex-grow-1"
th:href="@{/gruppen2/details/{id}/history(id=${group.getId()})}">Event-Log</a>
<a class="btn btn-info flex-grow-1 mt-2 mr-2 ml-auto"
th:href="@{/gruppen2/details/{id}/export/history/plain(id=${group.getId()})}"
title="Exportiert die gesamte Event-Historie dieser Gruppe. Kann beim erstellen importiert werden.">
Event-Log exportieren (TXT)
</a>
<a class="btn btn-info flex-grow-1 mt-2 mr-2"
th:href="@{/gruppen2/details/{id}/export/history/sql(id=${group.getId()})}"
title="Exportiert die gesamte Event-Historie dieser Gruppe. Kann als data.sql verwendet werden.">
Event-Log exportieren (SQL)
</a>
<a class="btn btn-info flex-grow-1 mt-2 mr-2"
th:href="@{/gruppen2/details/{id}/export/members(id=${group.getId()})}"
title="Exportiert die Teilnehmerliste. Kann beim erstellen (oder nachträglich) importiert werden.">
Teilnehmer exportieren
</a>
</div>
</div>
<!--Teilnehmerliste-->
<div class="content members py-2 px-1 px-sm-3 mx-n2 mx-sm-0">
<div class="content-heading">
<span>Teilnehmer</span>
</div>
<ul class="list-group">
<li class="list-group-item d-flex flex-row flex-wrap" th:each="member : ${group.getMembers()}">
<div class="overflow-hidden ml-n2" style="max-width: 50%; text-overflow: ellipsis;">
<span class="overflow-hidden ml-2" th:text="${member.format()}"></span>
<span th:replace="~{fragments/groups :: userbadges}"></span>
</div>
<div class="d-flex flex-row flex-wrap mt-n2 mr-n2 ml-auto" style="max-width: 50%;">
<div class="flex-grow-1 mt-2 mr-2"
th:unless="${member.getId() == account.getName()}">
<form th:action="@{/gruppen2/details/{id}/edit/delete/{userid}(id=${group.getId()}, userid=${member.getId()})}"
method="post">
<button type="submit" class="btn btn-danger w-100">
Entfernen
</button>
</form>
</div>
<div class="flex-grow-1 mt-2 mr-2">
<form th:action="@{/gruppen2/details/{id}/edit/role/{userid}(id=${group.getId()}, userid=${member.getId()})}"
method="post">
<button type="submit" class="btn btn-warning w-100">
Rolle ändern
</button>
</form>
</div>
</div>
</li>
</ul>
</div>
</main>

View File

@ -12,7 +12,7 @@
<main th:fragment="bodycontent">
<div class="container">
<h1 class="display-3 def-cursor">UPSI</h1>
<h1 class="display-3">UPSI</h1>
<div class="content">
<p class="lead def-cursor">Da ist wohl etwas schiefgelaufen :(</p>

View File

@ -13,7 +13,7 @@
<div class="input-group mb-2"
title="Ein Gruppentitel zwischen 4 und 128 Zeichen. Der Titel ist öffentlich.">
<div class="input-group-prepend">
<span class="input-group-text text-monospace">Gruppentitel:</span>
<span class="input-group-text">Gruppentitel:</span>
</div>
<input type="text" class="form-control" name="title" minlength="4" maxlength="128"
th:value="${group?.getTitle()}" required>
@ -24,7 +24,7 @@
<div class="input-group"
title="Eine kurze Gruppenbeschreibung zwischen 4 und 512 Zeichen. Die Beschreibung ist öffentlich.">
<div class="input-group-prepend">
<span class="input-group-text text-monospace">Beschreibung:</span>
<span class="input-group-text">Beschreibung:</span>
</div>
<textarea class="form-control" name="description" minlength="4" maxlength="512"
th:text="${group?.getDescription()}" required></textarea>
@ -34,8 +34,8 @@
<!--Gruppentyp-->
<th:block th:fragment="grouptype">
<label for="grouptype">Gruppentyp:</label>
<div class="btn-toolbar row mb-2 mx-0" id="grouptype">
<div class="btn-group btn-group-toggle col-sm-4 px-0" data-toggle="buttons">
<div class="btn-toolbar mt-n2 mr-n2 d-flex flex-row flex-wrap overflow-hidden" id="grouptype">
<div class="btn-group btn-group-toggle flex-grow-1 mt-2 mr-2" style="flex-basis: 10%;" data-toggle="buttons">
<label class="btn btn-secondary active" onclick="enable('#parentselect'); disable('#parentdummy')">
<input type="radio" name="type" value="PRIVATE" checked> Privat
</label>
@ -48,10 +48,10 @@
</label>
</div>
<div class="input-group col-sm-8 pr-0"
<div class="input-group flex-grow-1 mt-2 mr-2"
title="Optional kann eine Veranstaltungszugehörigkeit festgelegt werden.">
<div class="input-group-prepend">
<span class="input-group-text text-monospace">Gehört zu:</span>
<span class="input-group-text">Gehört zu:</span>
</div>
<input type="hidden" id="parentdummy" name="parent" value="00000000-0000-0000-0000-000000000000" disabled>
<select class="custom-select" id="parentselect" name="parent">
@ -66,10 +66,10 @@
<!--Userlimit-->
<th:block th:fragment="userlimit">
<label for="userlimit">Teilnehmeranzahl:</label>
<div class="btn-toolbar row mx-0" id="userlimit">
<div class="btn-toolbar mt-n2 mr-n2 d-flex flex-row flex-wrap flex-grow-1" id="userlimit">
<input type="hidden" name="limit" id="limit" value="999999"
th:disabled="${group != null && group.getLimit() < 999999} ? 'disabled' : 'false'">
<div class="btn-group btn-group-toggle col-sm-4 px-0" data-toggle="buttons">
<div class="btn-group btn-group-toggle flex-grow-1 mt-2 mr-2" style="flex-basis: 10%;" data-toggle="buttons">
<label class="btn btn-secondary active" onclick="disable('#limitselect'); enable('#limit')">
<input type="radio"
th:checked="${group != null && group.getLimit() < 999999} ? 'false' : 'checked'">
@ -82,17 +82,17 @@
</label>
</div>
<div class="input-group col-sm-8 pr-0"
<div class="input-group flex-grow-1 mt-2 mr-2"
title="999999 ist die maximal zulässige Teilnehmerzahl. Ist diese Obergrenze erreicht, gilt die Gruppe als unbegrenzt.">
<div class="input-group-prepend">
<span class="input-group-text text-monospace">Limit:</span>
<div class="input-group-prepend" style="max-width: 25%">
<span class="input-group-text">Limit:</span>
</div>
<input type="number" class="form-control" name="limit"
th:value="${group != null} ? ${group.getLimit()} : '999999'"
min="1" max="999999" id="limitselect" required
th:disabled="${group != null && group.getLimit() < 999999} ? 'false' : 'disabled'">
<div class="input-group-append">
<span class="input-group-text text-monospace">Teilnehmer</span>
<div class="input-group-append" style="max-width: 25%">
<span class="input-group-text">Teilnehmer</span>
</div>
</div>
</div>
@ -102,7 +102,7 @@
<div th:fragment="csvimport" class="input-group" th:if="${account.getRoles().contains('orga')}"
title="Das CSV folgt dem Format id,givenname,familyname,email.">
<div class="input-group-prepend">
<span class="input-group-text text-monospace">CSV:</span>
<span class="input-group-text">CSV:</span>
</div>
<div class="custom-file">
<input type="file" class="custom-file-input" id="file" name="file">

View File

@ -7,7 +7,6 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
<!--<script src="https://kit.fontawesome.com/22c0caaa8a.js" crossorigin="anonymous"></script>-->
<script type="text/javascript" th:src="@{/js/script.js}"></script>

View File

@ -7,17 +7,17 @@
<!--Grouptype Badges-->
<th:block th:fragment="badges">
<span class="badge badge-pill private"
<span class="badge badge-pill private align-self-start"
title="Kann nicht über die Suche gefunden werden, beitritt ist per Einladungslink möglich."
th:if='${group.isPrivate()}'>Privat</span>
<span class="badge badge-pill public"
<span class="badge badge-pill public align-self-start"
title="Kann über die Suche gefunden werden, jeder kann beitreten."
th:if="${group.isPublic()}">Öffentlich</span>
<span class="badge badge-pill lecture"
<span class="badge badge-pill lecture align-self-start"
title="Offizielle Veranstaltung"
th:if='${group.isLecture()}'>Veranstaltung</span>
<span class="badge badge-pill parent"
<span class="badge badge-pill parent align-self-start"
th:if="${parent?.exists()}"
th:title="${'Die Gruppe gehört zur Veranstaltung ' + parent.getTitle() + '.'}"
th:text="${parent.getTitle()}">Parent</span>
@ -36,7 +36,7 @@
<th:block th:fragment="groupcontent">
<!--Badges-->
<div class="content-heading">
<div class="content-heading overflow-hidden">
<span th:replace="~{fragments/groups :: badges}"></span>
</div>
@ -48,6 +48,19 @@
<!--<div class="body-text-in" th:if="${group.getMembers().contains(user.getId())}"></div>-->
</th:block>
<th:block th:fragment="groupcontentlink">
<div class="content-heading row overflow-hidden">
<a class="link col" th:href="@{/gruppen2/details/{id}(id=${group.getId()})}"
th:text="${group.getTitle()}"></a>
<span th:replace="~{fragments/groups :: badges}"></span>
</div>
<div class="content-text-in overflow-hidden" style="text-overflow: ellipsis;">
<span th:text="${group.getDescription()}"></span>
</div>
</th:block>
<!--Buttonbar zum Gruppe beitreten-->
<th:block th:fragment="joingroup">
<div class="content-heading">
@ -59,15 +72,13 @@
</span>
</div>
<div class="row">
<div class="d-flex flex-row flex-wrap">
<form method="post" th:action="@{/gruppen2/details/{id}/join(id = ${group.getId()})}"
th:unless="${group.isFull()}">
<button class="btn btn-success" type="submit">Gruppe beitreten.</button>
</form>
<div class="col" th:unless="${group.isFull()}"></div>
<a class="btn btn-primary" href="/gruppen2"
<a class="btn btn-primary ml-auto" href="/gruppen2"
type="submit">Startseite.</a>
</div>
</th:block>

View File

@ -9,23 +9,26 @@
<body>
<main th:fragment="bodycontent">
<div class="container-fluid">
<h1>Meine Gruppen</h1>
<h1 class="def-cursor">Meine Gruppen</h1>
<!--Gruppenliste belegte Gruppen-->
<div class="mx-n2 mx-sm-0" th:unless="${lectures.isEmpty()}">
<h3>Veranstaltungen</h3>
<!--Gruppenliste belegte Gruppen-->
<div class="content" th:each="group: ${groups}">
<div class="content-heading row">
<a class="link col" th:href="@{/gruppen2/details/{id}(id=${group.getId()})}"
th:text="${group.getTitle()}"></a>
<div class="content px-1 px-sm-3" th:each="group: ${lectures}"
th:insert="fragments/groups :: groupcontentlink"></div>
</div>
<span th:replace="~{fragments/groups :: badges}"></span>
</div>
<div class="content-text-in">
<span th:text="${group.getDescription()}"></span>
</div>
</div>
<div class="mx-n2 mx-sm-0" th:unless="${publics.isEmpty()}">
<h3>Öffentliche Gruppen</h3>
<div class="content px-1 px-sm-3" th:each="group: ${publics}"
th:insert="fragments/groups :: groupcontentlink"></div>
</div>
<div class="mx-n2 mx-sm-0" th:unless="${privates.isEmpty()}">
<h3>Private Gruppen</h3>
<div class="content px-1 px-sm-3" th:each="group: ${privates}"
th:insert="fragments/groups :: groupcontentlink"></div>
</div>
</main>

View File

@ -10,7 +10,7 @@
<main th:fragment="bodycontent">
<div class="container-fluid">
<h1 class="def-cursor" th:text="${group.getTitle()}"></h1>
<h1 th:text="${group.getTitle()}"></h1>
<div class="content" th:insert="~{fragments/groups :: groupcontent}"></div>

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org" th:replace="~{mopslayout :: html(
name='Gruppenfindung',
title='Event-Log',
headcontent=~{fragments/general :: headcontent('none')},
navigation=~{fragments/general :: nav('none')},
bodycontent=~{:: bodycontent})}">
<body>
<main th:fragment="bodycontent">
<h1>Event-Log</h1>
<div class="content py-2 px-1 px-sm-3 mx-n2 mx-sm-0" th:each="event : ${events}">
<div class="content-heading d-flex flex-row flex-wrap">
<span th:text="${event.type()}"></span>
<span class="ml-auto">Datum: </span>
<span th:text="${event.getDate()}"></span>
</div>
<div class="content-text-in px-1 px-sm-3">
<span th:text="${'User:' + event.getExec()}"></span>
<span th:text="${'>>>' + event.format()}"></span>
</div>
</div>
</main>
</body>
</html>

View File

@ -10,17 +10,15 @@
<main th:fragment="bodycontent">
<div class="container-fluid">
<h1 class="def-cursor" th:text="${group.getTitle()}"></h1>
<h1 th:text="${group.getTitle()}"></h1>
<div class="content" th:insert="~{fragments/groups :: groupcontent}"></div>
<div class="content" th:unless="${group.isPrivate()}"
th:insert="~{fragments/groups :: joingroup}"></div>
<div class="content row" th:if="${group.isPrivate()}">
<span class="col"></span>
<a class="btn btn-primary" href="/gruppen2">Startseite.</a>
<div class="content d-flex flex-row flex-wrap" th:if="${group.isPrivate()}">
<a class="btn btn-primary ml-auto" href="/gruppen2">Startseite.</a>
</div>
</div>
</main>

View File

@ -8,38 +8,44 @@
<body>
<!--/*@thymesVar id="LECTURE" type="mops.gruppen2.domain.model.group.Type"*/-->
<!--/*@thymesVar id="PUBLIC" type="mops.gruppen2.domain.model.group.Type"*/-->
<main th:fragment="bodycontent">
<div class="container-fluid">
<h1>Suchen</h1>
<h1>Suchen</h1>
<!--Suchfilter-->
<div class="content top">
<form method="post" th:action="@{/gruppen2/search}">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text text-monospace">Suchbegriff:</span>
<!--Suchfilter-->
<div class="content py-2 px-1 px-sm-3 mx-n2 mx-sm-0">
<form method="post" th:action="@{/gruppen2/search/string}">
<div class="d-flex flex-row flex-wrap mr-n2 mt-n2">
<div class="input-group mr-2 mt-2 flex-grow-1" style="flex-basis: 75%;">
<div class="input-group-prepend" style="max-width: 50%;">
<span class="input-group-text text-monospace overflow-hidden"
style="text-overflow: ellipsis">Suchbegriff:</span>
</div>
<input class="form-control" name="string" type="text">
<input class="form-control" required minlength="1" name="string" type="text">
</div>
<button class="btn btn-primary" type="submit">Suchen</button>
</form>
</div>
<!--Ergebnisliste-->
<div class="content" th:each="group: ${groups}">
<div class="content-heading row">
<span th:replace="~{fragments/groups :: badges}"></span>
<a class="link col" th:href="@{/gruppen2/details/{id}(id=${group.getId()})}"
th:text="${group.getTitle()}"></a>
<button class="btn btn-primary btn-block mr-2 mt-2 flex-grow-1" type="submit">
Suchen
</button>
</div>
<div class="content-text-in">
<span th:text="${group.getDescription()}"></span>
</div>
</div>
</form>
<div class="d-flex flex-row flex-wrap mt-1 mr-n2">
<a class="btn btn-info mt-2 mr-2 flex-grow-1"
th:href="@{/gruppen2/search/all}">Alle Anzeigen</a>
<a class="btn btn-info mt-2 mr-2 flex-grow-1 ml-auto"
th:href="@{/gruppen2/search/type/{type}(type=${LECTURE})}">Vorlesungen</a>
<a class="btn btn-info mt-2 mr-2 flex-grow-1"
th:href="@{/gruppen2/search/type/{type}(type=${PUBLIC})}">Öffentliche Gruppen</a>
</div>
</div>
<!--Ergebnisliste-->
<div class="content py-2 px-1 px-sm-3 mx-n2 mx-sm-0" th:each="group: ${groups}"
th:insert="fragments/groups :: groupcontentlink"></div>
</main>
</body>

View File

@ -0,0 +1,154 @@
package mops.gruppen2;
import mops.gruppen2.domain.event.AddMemberEvent;
import mops.gruppen2.domain.event.CreateGroupEvent;
import mops.gruppen2.domain.event.DestroyGroupEvent;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.event.KickMemberEvent;
import mops.gruppen2.domain.event.SetDescriptionEvent;
import mops.gruppen2.domain.event.SetInviteLinkEvent;
import mops.gruppen2.domain.event.SetLimitEvent;
import mops.gruppen2.domain.event.SetTitleEvent;
import mops.gruppen2.domain.event.SetTypeEvent;
import mops.gruppen2.domain.event.UpdateRoleEvent;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Role;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.infrastructure.GroupCache;
import java.time.LocalDateTime;
import java.util.UUID;
import static mops.gruppen2.TestHelper.uuid;
public final class GroupBuilder {
private final UUID groupid;
private int version;
private final GroupCache groupCache;
private final Group group = Group.EMPTY();
private GroupBuilder(GroupCache cache, UUID id) {
groupCache = cache;
groupid = id;
}
/**
* Erzeugt neuen GruppenBuilder mit Cache und ID
*/
public static GroupBuilder get(GroupCache cache, int id) {
return new GroupBuilder(cache, uuid(id));
}
/**
* Initialisiert Gruppe mit Id, Creator und Zeit
*/
public GroupBuilder group() {
return apply(new CreateGroupEvent(groupid, "TEST", LocalDateTime.now()));
}
/**
* Initialisiert TestAdmin
*/
public GroupBuilder testadmin() {
apply(new AddMemberEvent(groupid, "TEST", "TEST", new User("TEST")));
return apply(new UpdateRoleEvent(groupid, "TEST", "TEST", Role.ADMIN));
}
/**
* Fügt Nutzer hinzu
*/
public GroupBuilder add(String userid) {
return apply(new AddMemberEvent(groupid, "TEST", userid, new User(userid)));
}
/**
* Entfernt Nutzer
*/
public GroupBuilder kick(String userid) {
return apply(new KickMemberEvent(groupid, "TEST", userid));
}
public GroupBuilder limit(int i) {
return apply(new SetLimitEvent(groupid, "TEST", new Limit(i)));
}
/**
* Macht Nutzer zu Admin
*/
public GroupBuilder admin(String userid) {
return apply(new UpdateRoleEvent(groupid, "TEST", userid, Role.ADMIN));
}
/**
* Macht Nutzer zu regulärem
*/
public GroupBuilder regular(String userid) {
return apply(new UpdateRoleEvent(groupid, "TEST", userid, Role.REGULAR));
}
/**
* Macht Gruppe öffentlich
*/
public GroupBuilder publik() {
return apply(new SetTypeEvent(groupid, "TEST", Type.PUBLIC));
}
/**
* Macht Gruppe privat
*/
public GroupBuilder privat() {
return apply(new SetTypeEvent(groupid, "TEST", Type.PRIVATE));
}
/**
* Macht Gruppe zu Veranstaltung
*/
public GroupBuilder lecture() {
return apply(new SetTypeEvent(groupid, "TEST", Type.LECTURE));
}
/**
* Setzt Beschreibung
*/
public GroupBuilder desc(String descr) {
return apply(new SetDescriptionEvent(groupid, "TEST", new Description(descr)));
}
/**
* Setzt Titel
*/
public GroupBuilder title(String titl) {
return apply(new SetTitleEvent(groupid, "TEST", new Title(titl)));
}
/**
* Setzt Link
*/
public GroupBuilder link(int lnk) {
return apply(new SetInviteLinkEvent(groupid, "TEST", new Link(uuid(lnk).toString())));
}
/**
* Löscht Gruppe
*/
public GroupBuilder destroy() {
return apply(new DestroyGroupEvent(groupid, "TEST"));
}
public Group build() {
return group;
}
private GroupBuilder apply(Event event) {
version++;
event.init(version);
event.apply(group, groupCache);
return this;
}
}

View File

@ -1,297 +0,0 @@
package mops.gruppen2;
public class TestBuilder {
/*private static final Faker faker = new Faker();
public static Account account(String name) {
return new Account(name,
"",
"",
"",
"",
null);
}
public static Group apply(Group group, Event... events) {
for (Event event : events) {
event.apply(group);
}
return group;
}
public static Group apply(Event... events) {
return apply(new Group(), events);
}
*//**
* Baut eine UUID.
*
* @param id Integer id
*
* @return UUID
*//*
public static UUID uuidMock(int id) {
String idString = String.valueOf(Math.abs(id + 1));
return UUID.fromString("00000000-0000-0000-0000-"
+ "0".repeat(11 - idString.length())
+ idString);
}
*//**
* Generiert ein EventLog mit mehreren Gruppen und Usern.
*
* @param count Gruppenanzahl
* @param membercount Mitgliederanzahl pro Gruppe
*
* @return Eventliste
*//*
public static List<Event> completePublicGroups(int count, int membercount) {
return IntStream.range(0, count)
.parallel()
.mapToObj(i -> completePublicGroup(membercount))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
public static List<Event> completePrivateGroups(int count, int membercount) {
return IntStream.range(0, count)
.parallel()
.mapToObj(i -> completePrivateGroup(membercount))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
public static List<Event> completePublicGroup(int membercount) {
List<Event> eventList = new ArrayList<>();
UUID groupId = UUID.randomUUID();
eventList.add(createPublicGroupEvent(groupId));
eventList.add(updateGroupTitleEvent(groupId));
eventList.add(updateGroupDescriptionEvent(groupId));
eventList.add(new SetLimitEvent(groupId, "fgsadggas", new Limit(Long.MAX_VALUE)));
eventList.addAll(addUserEvents(membercount, groupId));
return eventList;
}
public static List<Event> completePrivateGroup(int membercount) {
List<Event> eventList = new ArrayList<>();
UUID groupId = UUID.randomUUID();
eventList.add(createPrivateGroupEvent(groupId));
eventList.add(updateGroupTitleEvent(groupId));
eventList.add(updateGroupDescriptionEvent(groupId));
eventList.add(new SetLimitEvent(groupId, "fgsadggas", new Limit(Long.MAX_VALUE)));
eventList.addAll(addUserEvents(membercount, groupId));
return eventList;
}
public static List<Event> completePublicGroup() {
return completePublicGroup(100);
}
public static List<Event> completePrivateGroup() {
return completePrivateGroup(100);
}
*//**
* Generiert mehrere CreateGroupEvents, 1 <= groupId <= count.
*
* @param count Anzahl der verschiedenen Gruppen
*
* @return Eventliste
*//*
public static List<Event> createPublicGroupEvents(int count) {
return IntStream.range(0, count)
.parallel()
.mapToObj(i -> createPublicGroupEvent())
.collect(Collectors.toList());
}
public static List<Event> createPrivateGroupEvents(int count) {
return IntStream.range(0, count)
.parallel()
.mapToObj(i -> createPublicGroupEvent())
.collect(Collectors.toList());
}
public static List<Event> createMixedGroupEvents(int count) {
return IntStream.range(0, count)
.parallel()
.mapToObj(i -> faker.random().nextInt(0, 1) > 0.5
? createPublicGroupEvent()
: createPrivateGroupEvent())
.collect(Collectors.toList());
}
public static List<Event> createPrivateGroupEvents(UUID groupId) {
return (new ArrayList<>()).addAll(createGroupEvent(groupId)),
new SetTypeEvent(groupId);
}
public static Event createPrivateGroupEvent() {
return createPrivateGroupEvent(UUID.randomUUID());
}
public static Event createPublicGroupEvent(UUID groupId) {
return createGroupEvent(groupId, Type.PUBLIC);
}
public static Event createPublicGroupEvent() {
return createPublicGroupEvent(UUID.randomUUID());
}
public static Event createGroupEvent(UUID groupId) {
return new CreateGroupEvent(groupId,
faker.random().hex(),
LocalDateTime.now());
}
public static Event createLectureEvent() {
return createLectureEvent(UUID.randomUUID());
}
public static Event createLectureEvent(UUID groupId) {
return new CreateGroupEvent(
groupId,
faker.random().hex(),
null,
Type.LECTURE
);
}
*//**
* Generiert mehrere AddUserEvents für eine Gruppe, 1 <= user_id <= count.
*
* @param count Anzahl der Mitglieder
* @param groupId Gruppe, zu welcher geaddet wird
*
* @return Eventliste
*//*
public static List<Event> addUserEvents(int count, UUID groupId) {
return IntStream.range(0, count)
.parallel()
.mapToObj(i -> addUserEvent(groupId, String.valueOf(i)))
.collect(Collectors.toList());
}
public static Event addUserEvent(UUID groupId, String userId) {
String firstname = firstname();
String lastname = lastname();
return new AddMemberEvent(
groupId,
userId,
firstname,
lastname,
firstname + "." + lastname + "@mail.de"
);
}
public static Event addUserEvent(UUID groupId) {
return addUserEvent(groupId, faker.random().hex());
}
// Am besten einfach nicht benutzen
public static List<Event> deleteUserEvents(int count, List<Event> eventList) {
List<Event> removeEvents = new ArrayList<>();
List<Event> shuffle = eventList.parallelStream()
.filter(event -> event instanceof AddMemberEvent)
.collect(Collectors.toList());
Collections.shuffle(shuffle);
for (Event event : shuffle) {
removeEvents.add(new KickMemberEvent(event.getGroupid(), event.getTarget()));
if (removeEvents.size() >= count) {
break;
}
}
return removeEvents;
}
*//**
* Erzeugt mehrere DeleteUserEvents, sodass eine Gruppe komplett geleert wird.
*
* @param group Gruppe welche geleert wird
*
* @return Eventliste
*//*
public static List<Event> deleteUserEvents(Group group) {
return group.getMemberships().parallelStream()
.map(user -> deleteUserEvent(group.getGroupid(), user.getUserId()))
.collect(Collectors.toList());
}
public static Event deleteUserEvent(UUID groupId, String userId) {
return new KickMemberEvent(
groupId,
userId
);
}
public static Event updateGroupDescriptionEvent(UUID groupId) {
return updateGroupDescriptionEvent(groupId, quote());
}
public static Event updateGroupDescriptionEvent(UUID groupId, String description) {
return new SetDescriptionEvent(
groupId,
faker.random().hex(),
description
);
}
public static Event updateGroupTitleEvent(UUID groupId) {
return updateGroupTitleEvent(groupId, champion());
}
public static Event updateGroupTitleEvent(UUID groupId, String title) {
return new SetTitleEvent(
groupId,
faker.random().hex(),
title
);
}
public static Event updateUserLimitMaxEvent(UUID groupId) {
return new SetLimitEvent(groupId, firstname(), Long.MAX_VALUE);
}
public static Event updateRoleEvent(UUID groupId, String userId, Role role) {
return new UpdateRoleEvent(
groupId,
userId,
role
);
}
public static Event deleteGroupEvent(UUID groupId) {
return new DestroyGroupEvent(groupId, faker.random().hex());
}
private static String firstname() {
return clean(faker.name().firstName());
}
private static String lastname() {
return clean(faker.name().lastName());
}
private static String champion() {
return clean(faker.leagueOfLegends().champion());
}
private static String quote() {
return clean(faker.leagueOfLegends().quote());
}
private static String clean(String string) {
return string.replaceAll("['\";,]", "");
}*/
}

View File

@ -0,0 +1,24 @@
package mops.gruppen2;
import mops.gruppen2.domain.event.Event;
import java.util.List;
import java.util.UUID;
public final class TestHelper {
public static UUID uuid(int id) {
String num = String.valueOf(id);
String string = "00000000-0000-0000-0000-";
string += "0".repeat(12 - num.length());
string += num;
return UUID.fromString(string);
}
public static void initEvents(List<Event> events) {
for (int i = 1; i <= events.size(); i++) {
events.get(i - 1).init(i);
}
}
}

View File

@ -0,0 +1,76 @@
package mops.gruppen2.domain.event;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.TestHelper;
import mops.gruppen2.domain.exception.GroupFullException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.exception.UserExistsException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
class AddMemberEventTest {
GroupCache cache;
@BeforeEach
void setUp() {
cache = new GroupCache(mock(EventStoreService.class));
}
@Test
void userMismatch() {
assertThatThrownBy(() -> new AddMemberEvent(TestHelper.uuid(1), "TEST", "TEST", new User("PETER")))
.isInstanceOf(IdMismatchException.class);
}
@Test
void apply() {
Group group = GroupBuilder.get(cache, 1).group().build();
Event add = new AddMemberEvent(TestHelper.uuid(1), "TEST", "TEST", new User("TEST"));
add.init(2);
add.apply(group);
assertThat(group.getMembers()).containsExactly(new User("TEST"));
}
@Test
void apply_cache() {
Group group = GroupBuilder.get(cache, 1).group().build();
Event add = new AddMemberEvent(TestHelper.uuid(1), "TEST", "TEST", new User("TEST"));
add.init(2);
add.apply(group, cache);
assertThat(group.getMembers()).containsExactly(new User("TEST"));
assertThat(cache.userGroups("TEST")).containsExactly(group);
}
@Test
void apply_userExists() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().limit(2).build();
Event add = new AddMemberEvent(TestHelper.uuid(1), "TEST", "TEST", new User("TEST"));
add.init(5);
assertThatThrownBy(() -> add.apply(group, cache))
.isInstanceOf(UserExistsException.class);
}
@Test
void apply_groupFull() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().build();
Event add = new AddMemberEvent(TestHelper.uuid(1), "TEST", "PETER", new User("PETER"));
add.init(4);
assertThatThrownBy(() -> add.apply(group, cache))
.isInstanceOf(GroupFullException.class);
}
}

View File

@ -0,0 +1,45 @@
package mops.gruppen2.domain.event;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static mops.gruppen2.TestHelper.uuid;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class CreateGroupEventTest {
GroupCache cache;
@BeforeEach
void setUp() {
cache = new GroupCache(mock(EventStoreService.class));
}
@Test
void apply() {
Group group = Group.EMPTY();
Event add = new CreateGroupEvent(uuid(1), "TEST", LocalDateTime.now());
add.init(1);
assertThat(group.exists()).isFalse();
add.apply(group);
assertThat(group.exists()).isTrue();
}
@Test
void apply_cache() {
Group group = Group.EMPTY();
Event add = new CreateGroupEvent(uuid(1), "TEST", LocalDateTime.now());
add.init(1);
add.apply(group, cache);
assertThat(group.exists()).isTrue();
assertThat(cache.groups()).hasSize(1);
}
}

View File

@ -0,0 +1,110 @@
package mops.gruppen2.domain.event;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static mops.gruppen2.TestHelper.uuid;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
class DestroyGroupEventTest {
GroupCache cache;
@BeforeEach
void setUp() {
cache = new GroupCache(mock(EventStoreService.class));
}
@Test
void apply() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().build();
Event destroy = new DestroyGroupEvent(uuid(1), "TEST");
destroy.init(4);
assertThat(group.exists()).isTrue();
destroy.apply(group);
assertThat(group.exists()).isFalse();
}
@Test
void apply_noadmin() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().limit(3).add("PETER").build();
Event destroy = new DestroyGroupEvent(uuid(1), "PETER");
destroy.init(6);
assertThatThrownBy(() -> destroy.apply(group))
.isInstanceOf(NoAccessException.class);
}
@Test
void apply_noadmin_empty() {
Group group = GroupBuilder.get(cache, 1).group().build();
Event destroy = new DestroyGroupEvent(uuid(1), "PETER");
destroy.init(2);
destroy.apply(group);
}
@Test
void apply_cache_private() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().privat().build();
Event destroy = new DestroyGroupEvent(uuid(1), "TEST");
destroy.init(5);
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userGroups("TEST")).hasSize(1);
assertThat(cache.privates()).hasSize(1);
destroy.apply(group, cache);
assertThat(cache.groups()).isEmpty();
assertThat(cache.userGroups("TEST")).isEmpty();
assertThat(cache.privates()).isEmpty();
}
@Test
void apply_cache_public() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().publik().build();
Event destroy = new DestroyGroupEvent(uuid(1), "TEST");
destroy.init(5);
assertThat(cache.publics()).hasSize(1);
destroy.apply(group, cache);
assertThat(cache.publics()).isEmpty();
}
@Test
void apply_cache_lecture() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().lecture().build();
Event destroy = new DestroyGroupEvent(uuid(1), "TEST");
destroy.init(5);
assertThat(cache.lectures()).hasSize(1);
destroy.apply(group, cache);
assertThat(cache.lectures()).isEmpty();
}
@Test
void apply_cache_multipleUsers() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().privat().limit(5).add("A").add("B").add("C").add("D").build();
Event destroy = new DestroyGroupEvent(uuid(1), "TEST");
destroy.init(10);
assertThat(cache.userGroups("TEST")).hasSize(1);
assertThat(cache.userGroups("A")).hasSize(1);
assertThat(cache.userGroups("B")).hasSize(1);
assertThat(cache.userGroups("C")).hasSize(1);
assertThat(cache.userGroups("D")).hasSize(1);
destroy.apply(group, cache);
assertThat(cache.userGroups("TEST")).hasSize(0);
assertThat(cache.userGroups("A")).hasSize(0);
assertThat(cache.userGroups("B")).hasSize(0);
assertThat(cache.userGroups("C")).hasSize(0);
assertThat(cache.userGroups("D")).hasSize(0);
}
}

View File

@ -0,0 +1,76 @@
package mops.gruppen2.domain.event;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static mops.gruppen2.TestHelper.uuid;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class EventTest {
@Test
void apply_smallVersion() {
Group group = GroupBuilder.get(Mockito.mock(GroupCache.class), 1).group().build();
Event add = new AddMemberEvent(uuid(1), "TEST", "TEST", new User("TEST"));
add.init(1);
assertThatThrownBy(() -> add.apply(group))
.isInstanceOf(IdMismatchException.class);
}
@Test
void apply_bigVersion() {
Group group = GroupBuilder.get(Mockito.mock(GroupCache.class), 1).group().build();
Event add = new AddMemberEvent(uuid(1), "TEST", "TEST", new User("TEST"));
add.init(3);
assertThatThrownBy(() -> add.apply(group))
.isInstanceOf(IdMismatchException.class);
}
@Test
void apply_notInitialized() {
Group group = GroupBuilder.get(Mockito.mock(GroupCache.class), 1).group().build();
Event add = new AddMemberEvent(uuid(1), "TEST", "TEST", new User("TEST"));
assertThatThrownBy(() -> add.apply(group))
.isInstanceOf(BadArgumentException.class);
}
@Test
void apply_wrongGroup() {
Group group = GroupBuilder.get(Mockito.mock(GroupCache.class), 1).group().build();
Event add = new AddMemberEvent(uuid(2), "TEST", "TEST", new User("TEST"));
add.init(2);
assertThatThrownBy(() -> add.apply(group))
.isInstanceOf(IdMismatchException.class);
}
@Test
void apply_updateVersion() {
Group group = GroupBuilder.get(Mockito.mock(GroupCache.class), 1).group().build();
Event add = new AddMemberEvent(uuid(1), "TEST", "TEST", new User("TEST"));
add.init(2);
assertThat(group.getVersion()).isEqualTo(1);
add.apply(group);
assertThat(group.getVersion()).isEqualTo(2);
}
@Test
void init_alreadyInitialized() {
Event add = new AddMemberEvent(uuid(1), "TEST", "TEST", new User("TEST"));
add.init(2);
assertThatThrownBy(() -> add.init(3))
.isInstanceOf(BadArgumentException.class);
}
}

View File

@ -0,0 +1,44 @@
package mops.gruppen2.domain.event;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static mops.gruppen2.TestHelper.uuid;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class KickMemberEventTest {
GroupCache cache;
@BeforeEach
void setUp() {
cache = new GroupCache(mock(EventStoreService.class));
}
@Test
void apply() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().build();
Event kick = new KickMemberEvent(uuid(1), "TEST", "TEST");
kick.init(4);
assertThat(group.size()).isOne();
kick.apply(group);
assertThat(group.size()).isZero();
}
@Test
void apply_cache() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().build();
Event kick = new KickMemberEvent(uuid(1), "TEST", "TEST");
kick.init(4);
assertThat(cache.userGroups("TEST")).hasSize(1);
kick.apply(group, cache);
assertThat(cache.userGroups("TEST")).hasSize(0);
}
}

View File

@ -0,0 +1,44 @@
package mops.gruppen2.domain.event;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static mops.gruppen2.TestHelper.uuid;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class SetInviteLinkEventTest {
GroupCache cache;
@BeforeEach
void setUp() {
cache = new GroupCache(mock(EventStoreService.class));
}
@Test
void apply() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().build();
Event link = new SetInviteLinkEvent(uuid(1), "TEST", new Link(uuid(2).toString()));
link.init(4);
link.apply(group);
assertThat(group.getLink()).isEqualTo(uuid(2).toString());
}
@Test
void apply_cache() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().build();
Event link = new SetInviteLinkEvent(uuid(1), "TEST", new Link(uuid(2).toString()));
link.init(4);
assertThat(cache.group(group.getLink())).isEqualTo(group);
link.apply(group, cache);
assertThat(cache.group(uuid(2).toString())).isEqualTo(group);
}
}

View File

@ -0,0 +1,34 @@
package mops.gruppen2.domain.event;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static mops.gruppen2.TestHelper.uuid;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
class SetLimitEventTest {
GroupCache cache;
@BeforeEach
void setUp() {
cache = new GroupCache(mock(EventStoreService.class));
}
@Test
void apply_tooSmall() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().limit(2).add("PETER").build();
Event limit = new SetLimitEvent(uuid(1), "TEST", new Limit(1));
limit.init(6);
assertThatThrownBy(() -> limit.apply(group))
.isInstanceOf(BadArgumentException.class);
}
}

View File

@ -0,0 +1,36 @@
package mops.gruppen2.domain.event;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static mops.gruppen2.TestHelper.uuid;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class SetTypeEventTest {
GroupCache cache;
@BeforeEach
void setUp() {
cache = new GroupCache(mock(EventStoreService.class));
}
@Test
void apply_cache() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().privat().build();
Event type = new SetTypeEvent(uuid(1), "TEST", Type.LECTURE);
type.init(5);
assertThat(cache.privates()).hasSize(1);
assertThat(cache.lectures()).isEmpty();
type.apply(group, cache);
assertThat(cache.lectures()).hasSize(1);
assertThat(cache.privates()).isEmpty();
}
}

View File

@ -0,0 +1,267 @@
package mops.gruppen2.domain.service;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.GroupFullException;
import mops.gruppen2.domain.exception.LastAdminException;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.exception.UserExistsException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static mops.gruppen2.TestHelper.uuid;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
class GroupServiceTest {
private GroupService groupService;
@BeforeEach
void setUp() {
groupService = new GroupService(mock(GroupCache.class), mock(EventStoreService.class));
}
@Test
void createGroup() {
Group group = groupService.createGroup("TEST");
assertThat(group.getId()).isNotNull();
assertThat(group.creator()).isEqualTo("TEST");
}
@Test
void initGroupMembers() {
Group group = groupService.createGroup("TEST");
groupService.initGroupMembers(group, "TEST", "TEST", new User("TEST"), new Limit(1));
assertThat(group.getMembers()).containsExactly(new User("TEST"));
assertThat(group.getLimit()).isEqualTo(1);
assertThat(group.isAdmin("TEST")).isTrue();
}
@Test
void initGroupMeta() {
Group group = groupService.createGroup("TEST");
groupService.initGroupMembers(group, "TEST", "TEST", new User("TEST"), new Limit(1));
groupService.initGroupMeta(group, "TEST", Type.PUBLIC, Parent.EMPTY());
assertThat(group.isPublic()).isTrue();
assertThat(group.hasParent()).isFalse();
}
@Test
void initGroupText() {
Group group = groupService.createGroup("TEST");
groupService.initGroupMembers(group, "TEST", "TEST", new User("TEST"), new Limit(1));
groupService.initGroupMeta(group, "TEST", Type.PUBLIC, Parent.EMPTY());
groupService.initGroupText(group, "TEST", new Title("TITLE"), new Description("DESCR"));
assertThat(group.getTitle()).isEqualTo("TITLE");
assertThat(group.getDescription()).isEqualTo("DESCR");
}
@Test
void addUsersToGroup() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
groupService.addUsersToGroup(group, "TEST", Arrays.asList(
new User("A"),
new User("B"),
new User("C"),
new User("C")));
assertThat(group.getLimit()).isEqualTo(4);
assertThat(group.size()).isEqualTo(4);
assertThat(group.getRegulars()).hasSize(3);
assertThat(group.getAdmins()).hasSize(1);
}
@Test
void toggleMemberRole_lastAdmin_lastUser() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
groupService.toggleMemberRole(group, "TEST", "TEST");
}
@Test
void toggleMemberRole_lastAdmin() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build();
assertThatThrownBy(() -> groupService.toggleMemberRole(group, "TEST", "TEST"))
.isInstanceOf(LastAdminException.class);
}
@Test
void toggleMemberRole_noMember() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
assertThatThrownBy(() -> groupService.toggleMemberRole(group, "TEST", "PETER"))
.isInstanceOf(UserNotFoundException.class);
}
@Test
void addMember_newMember() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).build();
groupService.addMember(group, "Test", "PETER", new User("PETER"));
assertThat(group.size()).isEqualTo(2);
assertThat(group.getAdmins()).hasSize(1);
assertThat(group.getRegulars()).hasSize(1);
}
@Test
void addMember_newMember_groupFull() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
assertThatThrownBy(() -> groupService.addMember(group, "Test", "PETER", new User("PETER")))
.isInstanceOf(GroupFullException.class);
}
@Test
void addMember_newMember_userExists() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(3).add("PETER").build();
assertThatThrownBy(() -> groupService.addMember(group, "Test", "PETER", new User("PETER")))
.isInstanceOf(UserExistsException.class);
}
@Test
void kickMember_noMember() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
assertThatThrownBy(() -> groupService.kickMember(group, "TEST", "PETER"))
.isInstanceOf(UserNotFoundException.class);
}
@Test
void kickMember_lastMember() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
groupService.kickMember(group, "TEST", "TEST");
assertThat(group.exists()).isFalse();
}
@Test
void kickMember_lastAdmin() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build();
assertThatThrownBy(() -> groupService.kickMember(group, "TEST", "TEST"))
.isInstanceOf(LastAdminException.class);
}
@Test
void deleteGroup_noGroup() {
groupService.deleteGroup(Group.EMPTY(), "TEST");
}
@Test
void deleteGroup() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build();
groupService.deleteGroup(group, "TEST");
assertThat(group.exists()).isFalse();
}
@Test
void deleteGroup_noAdmin() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build();
assertThatThrownBy(() -> groupService.deleteGroup(group, "PETER"))
.isInstanceOf(NoAccessException.class);
assertThat(group.exists()).isTrue();
}
@Test
void setTitle() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
groupService.setTitle(group, "TEST", new Title("TITLE"));
assertThat(group.getTitle()).isEqualTo("TITLE");
}
@Test
void setDescription() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
groupService.setDescription(group, "TEST", new Description("DESCR"));
assertThat(group.getDescription()).isEqualTo("DESCR");
}
@Test
void setLimit_tooLow() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build();
assertThatThrownBy(() -> groupService.setLimit(group, "TEST", new Limit(1)))
.isInstanceOf(BadArgumentException.class);
assertThat(group.getLimit()).isEqualTo(2);
}
@Test
void setLimit() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
groupService.setLimit(group, "TEST", new Limit(8));
assertThat(group.getLimit()).isEqualTo(8);
}
@Test
void setParent_sameGroup() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
assertThatThrownBy(() -> groupService.setParent(group, "TEST", new Parent(uuid(1).toString())))
.isInstanceOf(BadArgumentException.class);
assertThat(group.getParent()).isEqualTo(uuid(0));
assertThat(group.hasParent()).isFalse();
}
@Test
void setParent() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
groupService.setParent(group, "TEST", new Parent(uuid(2).toString()));
assertThat(group.getParent()).isEqualTo(uuid(2));
assertThat(group.hasParent()).isTrue();
}
@Test
void setLink_sameAsGroupId() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
assertThatThrownBy(() -> groupService.setLink(group, "TEST", new Link(uuid(1).toString())))
.isInstanceOf(BadArgumentException.class);
assertThat(group.getLink()).isNotEqualTo(uuid(1).toString());
}
@Test
void setLink() {
Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build();
groupService.setLink(group, "TEST", new Link(uuid(2).toString()));
assertThat(group.getLink()).isEqualTo(uuid(2).toString());
}
}

View File

@ -0,0 +1,116 @@
package mops.gruppen2.domain.service;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class SearchServiceTest {
private GroupCache groupCache;
private SearchService searchService;
@BeforeEach
void setUp() {
groupCache = new GroupCache(mock(EventStoreService.class));
searchService = new SearchService(groupCache);
}
@Test
void searchString_noResult() {
assertThat(searchService.searchString("", "TEST")).isEmpty();
}
@Test
void searchString_noResult_onePrivate_emptyString() {
GroupBuilder.get(groupCache, 1).group();
assertThat(searchService.searchString("", "TEST")).isEmpty();
}
@Test
void searchString_noResult_onePublic_emptyString_principalMember() {
GroupBuilder.get(groupCache, 1).group().testadmin().publik();
assertThat(searchService.searchString("", "TEST")).isEmpty();
}
@Test
void searchString_oneResult_onePublic_emptyString_principalNoMember() {
GroupBuilder.get(groupCache, 1).group().testadmin().publik();
assertThat(searchService.searchString("", "PETER")).hasSize(1);
}
@Test
void searchString_oneResult_multiple_emptyString() {
Group groupA = GroupBuilder.get(groupCache, 1).group().testadmin().lecture().build();
GroupBuilder.get(groupCache, 2).group().testadmin().publik().limit(2).add("PETER");
GroupBuilder.get(groupCache, 3).group().testadmin().publik().limit(2).add("PETER");
GroupBuilder.get(groupCache, 4).group().testadmin().privat();
assertThat(searchService.searchString("", "PETER")).containsExactly(groupA);
assertThat(searchService.searchString("", "TEST")).isEmpty();
}
@Test
void searchString_noPrivates() {
Group groupA = GroupBuilder.get(groupCache, 1).group().testadmin().lecture().build();
Group groupB = GroupBuilder.get(groupCache, 2).group().testadmin().publik().limit(2).add("PETER").build();
Group groupC = GroupBuilder.get(groupCache, 3).group().testadmin().publik().limit(2).add("PETER").build();
GroupBuilder.get(groupCache, 4).group().testadmin().privat();
GroupBuilder.get(groupCache, 5).group().testadmin().privat();
GroupBuilder.get(groupCache, 6).group().testadmin().privat();
assertThat(searchService.searchString("", "PETER")).containsExactly(groupA);
assertThat(searchService.searchString("", "PRRR")).containsOnly(groupA, groupB, groupC);
assertThat(searchService.searchString("", "TEST")).isEmpty();
}
@Test
void searchString_matchString_title() {
Group groupA = GroupBuilder.get(groupCache, 1).group().testadmin().lecture().title("A").build();
Group groupB = GroupBuilder.get(groupCache, 2).group().testadmin().lecture().title("B").build();
Group groupC = GroupBuilder.get(groupCache, 3).group().testadmin().lecture().title("C").build();
Group groupD = GroupBuilder.get(groupCache, 4).group().testadmin().lecture().title("CAESAR").build();
assertThat(searchService.searchString("C", "PETER")).containsExactly(groupC, groupD);
assertThat(searchService.searchString("C", "TEST")).isEmpty();
}
@Test
void searchString_matchString_desc() {
Group groupA = GroupBuilder.get(groupCache, 1).group().testadmin().lecture().desc("A").build();
Group groupB = GroupBuilder.get(groupCache, 2).group().testadmin().lecture().desc("B").build();
Group groupC = GroupBuilder.get(groupCache, 3).group().testadmin().lecture().desc("C").build();
Group groupD = GroupBuilder.get(groupCache, 4).group().testadmin().lecture().desc("CAESAR").build();
assertThat(searchService.searchString("C", "PETER")).containsExactly(groupC, groupD);
assertThat(searchService.searchString("C", "TEST")).isEmpty();
}
@Test
void searchType_noGroup() {
assertThat(searchService.searchType(Type.LECTURE, "PETER")).isEmpty();
assertThat(searchService.searchType(Type.PUBLIC, "PETER")).isEmpty();
assertThat(searchService.searchType(Type.PRIVATE, "PETER")).isEmpty();
}
@Test
void searchType_noPrivates() {
GroupBuilder.get(groupCache, 1).group().testadmin().lecture();
GroupBuilder.get(groupCache, 2).group().testadmin().publik();
GroupBuilder.get(groupCache, 3).group().testadmin().privat();
GroupBuilder.get(groupCache, 4).group().testadmin().privat();
GroupBuilder.get(groupCache, 5).group().testadmin().lecture();
assertThat(searchService.searchType(Type.LECTURE, "PETER")).hasSize(2);
assertThat(searchService.searchType(Type.PUBLIC, "PETER")).hasSize(1);
assertThat(searchService.searchType(Type.PRIVATE, "PETER")).isEmpty();
}
}

View File

@ -0,0 +1,243 @@
package mops.gruppen2.domain.service.helper;
import mops.gruppen2.domain.event.AddMemberEvent;
import mops.gruppen2.domain.event.CreateGroupEvent;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.event.SetLimitEvent;
import mops.gruppen2.domain.event.UpdateRoleEvent;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Role;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static mops.gruppen2.TestHelper.initEvents;
import static mops.gruppen2.TestHelper.uuid;
import static mops.gruppen2.domain.service.helper.ProjectionHelper.project;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class ProjectionHelperTest {
@BeforeEach
void setUp() {
}
@Test
void project_nocache_emptyList() {
assertThat(project(Collections.emptyList())).isEmpty();
}
@Test
void project_nocache_oneCreate() {
List<Event> events = Arrays.asList(
new CreateGroupEvent(uuid(1), "TEST", LocalDateTime.now()));
initEvents(events);
assertThat(project(events)).hasSize(1);
}
@Test
void project_nocache_multipleCreate() {
List<Event> events = Arrays.asList(
new CreateGroupEvent(uuid(1), "TEST", LocalDateTime.now()),
new CreateGroupEvent(uuid(2), "TEST", LocalDateTime.now()),
new CreateGroupEvent(uuid(3), "TEST", LocalDateTime.now()),
new CreateGroupEvent(uuid(4), "TEST", LocalDateTime.now()),
new CreateGroupEvent(uuid(5), "TEST", LocalDateTime.now()));
events.get(0).init(1);
events.get(1).init(1);
events.get(2).init(1);
events.get(3).init(1);
events.get(4).init(1);
assertThat(project(events)).hasSize(5);
}
@Test
void project_nocache_oneDetailed() {
List<Event> events = Arrays.asList(
new CreateGroupEvent(uuid(1), "TEST", LocalDateTime.now()),
new AddMemberEvent(uuid(1), "TEST", "TEST", new User("TEST")),
new UpdateRoleEvent(uuid(1), "TEST", "TEST", Role.ADMIN),
new SetLimitEvent(uuid(1), "TEST", new Limit(5)));
initEvents(events);
List<Group> groups = project(events);
assertThat(groups).hasSize(1);
assertThat(groups.get(0).exists()).isTrue();
assertThat(groups.get(0).getAdmins()).hasSize(1);
assertThat(groups.get(0).getLimit()).isEqualTo(5);
}
@Test
void project_nocache_multipleDetailed() {
List<Event> eventsA = Arrays.asList(
new CreateGroupEvent(uuid(1), "TEST", LocalDateTime.now()),
new AddMemberEvent(uuid(1), "TEST", "TEST", new User("TEST")),
new UpdateRoleEvent(uuid(1), "TEST", "TEST", Role.ADMIN),
new SetLimitEvent(uuid(1), "TEST", new Limit(5)));
List<Event> eventsB = Arrays.asList(
new CreateGroupEvent(uuid(2), "TEST", LocalDateTime.now()),
new AddMemberEvent(uuid(2), "TEST", "TEST", new User("TEST")),
new UpdateRoleEvent(uuid(2), "TEST", "TEST", Role.ADMIN),
new SetLimitEvent(uuid(2), "TEST", new Limit(15)));
List<Event> eventsC = Arrays.asList(
new CreateGroupEvent(uuid(3), "TEST", LocalDateTime.now()),
new AddMemberEvent(uuid(3), "TEST", "TEST", new User("TEST")),
new UpdateRoleEvent(uuid(3), "TEST", "TEST", Role.ADMIN),
new SetLimitEvent(uuid(3), "TEST", new Limit(25)));
List<Event> eventsD = Arrays.asList(
new CreateGroupEvent(uuid(4), "TEST", LocalDateTime.now()),
new AddMemberEvent(uuid(4), "TEST", "TEST", new User("TEST")),
new UpdateRoleEvent(uuid(4), "TEST", "TEST", Role.ADMIN),
new SetLimitEvent(uuid(4), "TEST", new Limit(35)));
initEvents(eventsA);
initEvents(eventsB);
initEvents(eventsC);
initEvents(eventsD);
List<Event> events = new ArrayList<>();
events.addAll(eventsA);
events.addAll(eventsB);
events.addAll(eventsC);
events.addAll(eventsD);
List<Group> groups = project(events);
assertThat(groups).hasSize(4);
assertThat(groups.get(0).exists()).isTrue();
assertThat(groups.get(0).getAdmins()).hasSize(1);
assertThat(groups.get(0).getLimit()).isEqualTo(5);
assertThat(groups.get(1).exists()).isTrue();
assertThat(groups.get(1).getAdmins()).hasSize(1);
assertThat(groups.get(1).getLimit()).isEqualTo(15);
assertThat(groups.get(2).exists()).isTrue();
assertThat(groups.get(2).getAdmins()).hasSize(1);
assertThat(groups.get(2).getLimit()).isEqualTo(25);
assertThat(groups.get(3).exists()).isTrue();
assertThat(groups.get(3).getAdmins()).hasSize(1);
assertThat(groups.get(3).getLimit()).isEqualTo(35);
}
@Test
void project_cache_noGroups() {
Map<UUID, Group> groups = new HashMap<>();
project(groups, Collections.emptyList(), mock(GroupCache.class));
assertThat(groups).isEmpty();
}
@Test
void project_cache_oneCreate() {
Map<UUID, Group> groups = new HashMap<>();
List<Event> events = Arrays.asList(
new CreateGroupEvent(uuid(1), "TEST", LocalDateTime.now()));
initEvents(events);
project(groups, events, mock(GroupCache.class));
assertThat(groups).hasSize(1);
assertThat(groups.keySet()).containsExactly(uuid(1));
}
@Test
void project_cache_multipleCreate() {
Map<UUID, Group> groups = new HashMap<>();
List<Event> events = Arrays.asList(
new CreateGroupEvent(uuid(1), "TEST", LocalDateTime.now()),
new CreateGroupEvent(uuid(2), "TEST", LocalDateTime.now()),
new CreateGroupEvent(uuid(3), "TEST", LocalDateTime.now()),
new CreateGroupEvent(uuid(4), "TEST", LocalDateTime.now()));
events.get(0).init(1);
events.get(1).init(1);
events.get(2).init(1);
events.get(3).init(1);
project(groups, events, mock(GroupCache.class));
assertThat(groups).hasSize(4);
assertThat(groups.keySet()).containsExactly(uuid(1), uuid(2), uuid(3), uuid(4));
}
@Test
void project_cache_multipleDetailed() {
Map<UUID, Group> groups = new HashMap<>();
List<Event> eventsA = Arrays.asList(
new CreateGroupEvent(uuid(1), "TEST", LocalDateTime.now()),
new AddMemberEvent(uuid(1), "TEST", "TEST", new User("TEST")),
new UpdateRoleEvent(uuid(1), "TEST", "TEST", Role.ADMIN),
new SetLimitEvent(uuid(1), "TEST", new Limit(5)));
List<Event> eventsB = Arrays.asList(
new CreateGroupEvent(uuid(2), "TEST", LocalDateTime.now()),
new AddMemberEvent(uuid(2), "TEST", "TEST", new User("TEST")),
new UpdateRoleEvent(uuid(2), "TEST", "TEST", Role.ADMIN),
new SetLimitEvent(uuid(2), "TEST", new Limit(15)));
List<Event> eventsC = Arrays.asList(
new CreateGroupEvent(uuid(3), "TEST", LocalDateTime.now()),
new AddMemberEvent(uuid(3), "TEST", "TEST", new User("TEST")),
new UpdateRoleEvent(uuid(3), "TEST", "TEST", Role.ADMIN),
new SetLimitEvent(uuid(3), "TEST", new Limit(25)));
List<Event> eventsD = Arrays.asList(
new CreateGroupEvent(uuid(4), "TEST", LocalDateTime.now()),
new AddMemberEvent(uuid(4), "TEST", "TEST", new User("TEST")),
new UpdateRoleEvent(uuid(4), "TEST", "TEST", Role.ADMIN),
new SetLimitEvent(uuid(4), "TEST", new Limit(35)));
initEvents(eventsA);
initEvents(eventsB);
initEvents(eventsC);
initEvents(eventsD);
List<Event> events = new ArrayList<>();
events.addAll(eventsA);
events.addAll(eventsB);
events.addAll(eventsC);
events.addAll(eventsD);
project(groups, events, mock(GroupCache.class));
assertThat(groups).hasSize(4);
assertThat(groups.get(uuid(1)).exists()).isTrue();
assertThat(groups.get(uuid(1)).getAdmins()).hasSize(1);
assertThat(groups.get(uuid(1)).getLimit()).isEqualTo(5);
assertThat(groups.get(uuid(2)).exists()).isTrue();
assertThat(groups.get(uuid(2)).getAdmins()).hasSize(1);
assertThat(groups.get(uuid(2)).getLimit()).isEqualTo(15);
assertThat(groups.get(uuid(3)).exists()).isTrue();
assertThat(groups.get(uuid(3)).getAdmins()).hasSize(1);
assertThat(groups.get(uuid(3)).getLimit()).isEqualTo(25);
assertThat(groups.get(uuid(4)).exists()).isTrue();
assertThat(groups.get(uuid(4)).getAdmins()).hasSize(1);
assertThat(groups.get(uuid(4)).getLimit()).isEqualTo(35);
}
}

View File

@ -0,0 +1,312 @@
package mops.gruppen2.infrastructure;
import mops.gruppen2.GroupBuilder;
import mops.gruppen2.domain.exception.GroupNotFoundException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.service.EventStoreService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static mops.gruppen2.TestHelper.uuid;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
// Kann nur indirket über events getestet werden, diese werden also "mitgetestet"
class GroupCacheTest {
private GroupCache cache;
@BeforeEach
void setUp() {
cache = new GroupCache(Mockito.mock(EventStoreService.class));
}
@Test
void groups_noGroups() {
assertThat(cache.groups()).isEmpty();
}
@Test
void group_groupNotFound() {
assertThatThrownBy(() -> cache.group(uuid(1)))
.isInstanceOf(GroupNotFoundException.class);
}
@Test
void group_linkNotFound() {
assertThatThrownBy(() -> cache.group("00000000-0000-0000-0000-000000000000"))
.isInstanceOf(GroupNotFoundException.class);
}
@Test
void group_groupFound() {
Group group = GroupBuilder.get(cache, 1).group().build();
assertThat(cache.group(uuid(1))).isEqualTo(group);
}
@Test
void group_linkFound() {
Group group = GroupBuilder.get(cache, 1).group().build();
assertThat(cache.group(group.getLink())).isEqualTo(group);
}
@Test
void userGroups_noGroups() {
assertThat(cache.userGroups("TEST")).isEmpty();
}
@Test
void userGroups_noUserGroups() {
Group group = GroupBuilder.get(cache, 1).group().add("PETER").build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userGroups("TEST")).isEmpty();
}
@Test
void userGroups_oneUserGroup() {
Group group = GroupBuilder.get(cache, 1).group().add("TEST").build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userGroups("TEST")).containsExactly(group);
}
@Test
void userGroups_userGroup_multiple() {
Group groupA = GroupBuilder.get(cache, 1).group().add("TEST").build();
Group groupB = GroupBuilder.get(cache, 2).group().add("PETER").build();
Group groupC = GroupBuilder.get(cache, 3).group().add("TEST").build();
Group groupD = GroupBuilder.get(cache, 4).group().add("PETER").build();
assertThat(cache.groups()).hasSize(4);
assertThat(cache.userGroups("PETER")).containsExactly(groupB, groupD);
}
@Test
void userLectures_noGroups() {
assertThat(cache.userLectures("PETER")).isEmpty();
}
@Test
void userLectures_noLecture() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().publik().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userLectures("PETER")).isEmpty();
}
@Test
void userLectures_oneLecture() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().limit(2).add("PETER").lecture().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userLectures("PETER")).containsExactly(group);
}
@Test
void userLectures_lecture_multiple() {
Group groupA = GroupBuilder.get(cache, 1).group().testadmin().limit(2).add("PETER").lecture().build();
Group groupB = GroupBuilder.get(cache, 2).group().testadmin().limit(2).add("PETER").publik().build();
Group groupC = GroupBuilder.get(cache, 3).group().testadmin().limit(2).add("PETER").privat().build();
Group groupD = GroupBuilder.get(cache, 4).group().testadmin().lecture().build();
assertThat(cache.groups()).hasSize(4);
assertThat(cache.userLectures("PETER")).containsExactly(groupA);
}
@Test
void userPublics_noGroups() {
assertThat(cache.userPublics("PETER")).isEmpty();
}
@Test
void userPublics_noPublic() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().limit(2).add("PETER").publik().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userLectures("PETER")).isEmpty();
}
@Test
void userPublics_onePublic() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().limit(2).add("PETER").lecture().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userLectures("PETER")).containsExactly(group);
}
@Test
void userPublics_public_multiple() {
Group groupA = GroupBuilder.get(cache, 1).group().testadmin().limit(2).add("PETER").lecture().build();
Group groupB = GroupBuilder.get(cache, 2).group().testadmin().limit(2).add("PETER").publik().build();
Group groupC = GroupBuilder.get(cache, 3).group().testadmin().limit(2).add("PETER").privat().build();
Group groupD = GroupBuilder.get(cache, 4).group().testadmin().publik().build();
assertThat(cache.groups()).hasSize(4);
assertThat(cache.userPublics("PETER")).containsExactly(groupB);
}
@Test
void userPrivates_noGroups() {
assertThat(cache.userPrivates("PETER")).isEmpty();
}
@Test
void userPrivates_noPrivate() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().limit(2).add("PETER").publik().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userPrivates("PETER")).isEmpty();
}
@Test
void userPrivates_onePrivate() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().limit(2).add("PETER").privat().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userPrivates("PETER")).containsExactly(group);
}
@Test
void userPrivates_private_multiple() {
Group groupA = GroupBuilder.get(cache, 1).group().testadmin().limit(2).add("PETER").lecture().build();
Group groupB = GroupBuilder.get(cache, 2).group().testadmin().limit(2).add("PETER").privat().build();
Group groupC = GroupBuilder.get(cache, 3).group().testadmin().privat().build();
Group groupD = GroupBuilder.get(cache, 4).group().testadmin().publik().build();
assertThat(cache.groups()).hasSize(4);
assertThat(cache.userPrivates("PETER")).containsExactly(groupB);
}
@Test
void publics_noGroups() {
assertThat(cache.publics()).isEmpty();
}
@Test
void publics_noPublic() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().privat().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.publics()).isEmpty();
}
@Test
void publics_onePublic() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().publik().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.publics()).containsExactly(group);
}
@Test
void publics_public_multiple() {
Group groupA = GroupBuilder.get(cache, 1).group().testadmin().lecture().build();
Group groupB = GroupBuilder.get(cache, 2).group().testadmin().privat().build();
Group groupC = GroupBuilder.get(cache, 3).group().testadmin().privat().build();
Group groupD = GroupBuilder.get(cache, 4).group().testadmin().publik().build();
assertThat(cache.groups()).hasSize(4);
assertThat(cache.publics()).containsExactly(groupD);
}
@Test
void privates_noGroups() {
assertThat(cache.privates()).isEmpty();
}
@Test
void privates_noPrivate() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().publik().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.privates()).isEmpty();
}
@Test
void privates_onePrivate() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().privat().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.privates()).containsExactly(group);
}
@Test
void privates_private_multiple() {
Group groupA = GroupBuilder.get(cache, 1).group().testadmin().lecture().build();
Group groupB = GroupBuilder.get(cache, 2).group().testadmin().privat().build();
Group groupC = GroupBuilder.get(cache, 3).group().testadmin().privat().build();
Group groupD = GroupBuilder.get(cache, 4).group().testadmin().publik().build();
assertThat(cache.groups()).hasSize(4);
assertThat(cache.privates()).containsExactly(groupB, groupC);
}
@Test
void lectures_noGroups() {
assertThat(cache.lectures()).isEmpty();
}
@Test
void lectures_noLecture() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().privat().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.lectures()).isEmpty();
}
@Test
void lectures_oneLecture() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().lecture().build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.lectures()).containsExactly(group);
}
@Test
void lectures_lecture_multiple() {
Group groupA = GroupBuilder.get(cache, 1).group().testadmin().lecture().build();
Group groupB = GroupBuilder.get(cache, 2).group().testadmin().privat().build();
Group groupC = GroupBuilder.get(cache, 3).group().testadmin().lecture().build();
Group groupD = GroupBuilder.get(cache, 4).group().testadmin().publik().build();
assertThat(cache.groups()).hasSize(4);
assertThat(cache.lectures()).containsExactly(groupA, groupC);
}
//Indirekt: void usersPut() {}
@Test
void usersRemove() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().lecture().limit(2).add("PETER").kick("PETER").build();
assertThat(cache.groups()).hasSize(1);
assertThat(cache.userLectures("PETER")).isEmpty();
}
//Indirekt: void groupsPut() {}
@Test
void groupsRemove() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().lecture().destroy().build();
assertThat(cache.groups()).hasSize(0);
}
//Indirekt: void linksPut() {}
@Test
void linksRemove() {
Group group = GroupBuilder.get(cache, 1).group().testadmin().lecture().link(2).build();
assertThat(cache.group(String.valueOf(uuid(2)))).isEqualTo(group);
}
//Indirekt: void typesPut() {}
//Indirekt: void typesRemove() {}
}

View File

@ -0,0 +1,53 @@
package mops.gruppen2.infrastructure.controller;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.infrastructure.GroupCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.security.test.context.support.WithMockUser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class APIControllerTest {
private EventStoreService store;
private GroupCache cache;
private APIController controller;
@BeforeEach
void setUp() {
store = mock(EventStoreService.class);
cache = new GroupCache(store);
controller = new APIController(cache, store);
}
@WithMockUser("ROLE_api_user")
@Test
void getApiUpdate_noEvents() {
when(store.findMaxEventId()).thenReturn(0L);
assertThat(controller.getApiUpdate(0).getVersion()).isZero();
assertThat(controller.getApiUpdate(0).getGroups()).isEmpty();
}
@Disabled
@WithMockUser("ROLE_api_user")
@Test
void getApiUpdate_noUpdate() {
}
@Disabled
@WithMockUser("ROLE_api_user")
@Test
void getApiUserGroups() {
}
@Disabled
@WithMockUser("ROLE_api_user")
@Test
void getApiGroup() {
}
}

View File

@ -0,0 +1,19 @@
package mops.gruppen2.infrastructure.controller;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class GroupCreationControllerTest {
@BeforeEach
void setUp() {
}
@Test
void getCreate() {
}
@Test
void postCreateOrga() {
}
}

View File

@ -0,0 +1,67 @@
package mops.gruppen2.infrastructure.controller;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class GroupDetailsControllerTest {
@BeforeEach
void setUp() {
}
@Test
void getDetailsPage() {
}
@Test
void postDetailsJoin() {
}
@Test
void postDetailsLeave() {
}
@Test
void getDetailsHistory() {
}
@Test
void getDetailsExportHistoryPlain() {
}
@Test
void getDetailsExportHistorySql() {
}
@Test
void getDetailsExportMembers() {
}
@Test
void getDetailsEdit() {
}
@Test
void postDetailsEditMeta() {
}
@Test
void postDetailsEditUserLimit() {
}
@Test
void postDetailsEditCsv() {
}
@Test
void postDetailsEditRole() {
}
@Test
void postDetailsEditDelete() {
}
@Test
void postDetailsEditDestroy() {
}
}

View File

@ -0,0 +1,31 @@
package mops.gruppen2.infrastructure.controller;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class SearchAndInviteControllerTest {
@BeforeEach
void setUp() {
}
@Test
void getSearch() {
}
@Test
void postSearchString() {
}
@Test
void getSearchAll() {
}
@Test
void getSearchType() {
}
@Test
void getJoin() {
}
}