diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 2dc2b710..2e59b043 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -131,6 +131,11 @@ public final class API { public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT = "/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}"; + public static final String SEB_CLIENT_CONNECTION_ENDPOINT = "/seb-client-connection"; + public static final String SEB_CLIENT_EVENT_ENDPOINT = "/seb-client-event"; + public static final String SEB_CLIENT_EVENT_SEARCH_PATH_SEGMENT = "/search"; + public static final String SEB_CLIENT_EVENT_EXTENDED_PAGE_ENDPOINT = SEB_CLIENT_EVENT_ENDPOINT + + SEB_CLIENT_EVENT_SEARCH_PATH_SEGMENT; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/Entity.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/Entity.java index 0b2a8b1e..d701fbd9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/Entity.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/Entity.java @@ -50,9 +50,19 @@ public interface Entity extends ModelIdAware { * @return EntityName instance created form given Entity */ default EntityName toName() { return new EntityName( - this.entityType(), this.getModelId(), + this.entityType(), this.getName()); } + /** This can be overwritten if an entity contains security sensitive data + * Returns a representation of the entity that has no security sensitive data + * and an be print out to user logs or error messages + * + * @return representation of the entity that has no security sensitive data */ + + default Entity printSecureCopy() { + return this; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKey.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKey.java index 3bc7d405..a1b0347b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKey.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKey.java @@ -13,12 +13,14 @@ import java.io.Serializable; import javax.validation.constraints.NotNull; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import ch.ethz.seb.sebserver.gbl.api.EntityType; /** A EntityKey uniquely identifies a domain entity within the SEB Server's domain model. * A EntityKey consists of the model identifier of a domain entity and the type of the entity. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class EntityKey implements ModelIdAware, Serializable { private static final long serialVersionUID = -2368065921846821061L; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityName.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityName.java index 15762cc5..913d6e0b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityName.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityName.java @@ -17,33 +17,30 @@ import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.EntityType; @JsonIgnoreProperties(ignoreUnknown = true) -public class EntityName implements ModelIdAware { +public class EntityName extends EntityKey { + + private static final long serialVersionUID = 9577137222563155L; - @JsonProperty(value = API.PARAM_ENTITY_TYPE, required = true) - public final EntityType entityType; - @JsonProperty(value = API.PARAM_MODEL_ID, required = true) - public final String modelId; @JsonProperty(value = "name", required = true) public final String name; @JsonCreator public EntityName( - @JsonProperty(value = API.PARAM_ENTITY_TYPE, required = true) final EntityType entityType, @JsonProperty(value = API.PARAM_MODEL_ID, required = true) final String id, + @JsonProperty(value = API.PARAM_ENTITY_TYPE, required = true) final EntityType entityType, @JsonProperty(value = "name", required = true) final String name) { - this.entityType = entityType; - this.modelId = id; + super(id, entityType); this.name = name; } public EntityName(final EntityKey entityKey, final String name) { - this.entityType = entityKey.entityType; - this.modelId = entityKey.modelId; + super(entityKey.modelId, entityKey.entityType); this.name = name; } + @Override public EntityType getEntityType() { return this.entityType; } @@ -63,40 +60,6 @@ public class EntityName implements ModelIdAware { return new EntityKey(getModelId(), getEntityType()); } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((this.entityType == null) ? 0 : this.entityType.hashCode()); - result = prime * result + ((this.modelId == null) ? 0 : this.modelId.hashCode()); - result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - final EntityName other = (EntityName) obj; - if (this.entityType != other.entityType) - return false; - if (this.modelId == null) { - if (other.modelId != null) - return false; - } else if (!this.modelId.equals(other.modelId)) - return false; - if (this.name == null) { - if (other.name != null) - return false; - } else if (!this.name.equals(other.name)) - return false; - return true; - } - @Override public String toString() { final StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 4ebff91a..6cc246f2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Activatable; @@ -30,6 +31,23 @@ import ch.ethz.seb.sebserver.gbl.model.GrantEntity; @JsonIgnoreProperties(ignoreUnknown = true) public final class Exam implements GrantEntity, Activatable { + public static final Exam EMPTY_EXAM = new Exam( + -1L, + -1L, + -1L, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE, + null, + null, + Constants.EMPTY_NOTE, + ExamType.UNDEFINED, + null, + null, + null, + null, + false); + // TODO make this a configurable exam attribute /** The number of hours to add at the start- and end-time of the exam * To add a expanded time-frame in which the exam state is running on SEB-Server side */ diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamConfigurationMap.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamConfigurationMap.java index 07994e52..4c2a863a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamConfigurationMap.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamConfigurationMap.java @@ -15,11 +15,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Domain.CONFIGURATION_NODE; import ch.ethz.seb.sebserver.gbl.model.Domain.EXAM_CONFIGURATION_MAP; +import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.GrantEntity; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; @@ -143,10 +145,12 @@ public final class ExamConfigurationMap implements GrantEntity { return this.userNames; } + @JsonIgnore public CharSequence getEncryptSecret() { return this.encryptSecret; } + @JsonIgnore public CharSequence getConfirmEncryptSecret() { return this.confirmEncryptSecret; } @@ -168,6 +172,21 @@ public final class ExamConfigurationMap implements GrantEntity { return this.configStatus; } + @Override + public Entity printSecureCopy() { + return new ExamConfigurationMap( + this.id, + this.institutionId, + this.examId, + this.configurationNodeId, + this.userNames, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE, + this.configName, + this.configDescription, + this.configStatus); + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java index 3d3eec19..5f80cc9d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java @@ -18,11 +18,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.gbl.model.Domain.INSTITUTION; import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; +import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.EntityName; import ch.ethz.seb.sebserver.gbl.model.GrantEntity; @@ -161,6 +163,20 @@ public final class LmsSetup implements GrantEntity, Activatable { return this.active; } + @Override + public Entity printSecureCopy() { + return new LmsSetup( + this.id, + this.institutionId, + this.name, + this.lmsType, + this.lmsAuthName, + Constants.EMPTY_NOTE, + this.lmsApiUrl, + Constants.EMPTY_NOTE, + this.active); + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); @@ -174,8 +190,6 @@ public final class LmsSetup implements GrantEntity, Activatable { builder.append(this.lmsType); builder.append(", lmsAuthName="); builder.append(this.lmsAuthName); - builder.append(", lmsAuthSecret="); - builder.append(this.lmsAuthSecret); builder.append(", lmsApiUrl="); builder.append(this.lmsApiUrl); builder.append(", lmsRestApiToken="); @@ -188,8 +202,8 @@ public final class LmsSetup implements GrantEntity, Activatable { public static EntityName toName(final LmsSetup lmsSetup) { return new EntityName( - EntityType.LMS_SETUP, String.valueOf(lmsSetup.id), + EntityType.LMS_SETUP, lmsSetup.name); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/SebClientConfig.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/SebClientConfig.java index be3bc3df..f8739c8e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/SebClientConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/SebClientConfig.java @@ -15,14 +15,17 @@ import org.hibernate.validator.constraints.URL; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Domain.SEB_CLIENT_CONFIGURATION; +import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.GrantEntity; public final class SebClientConfig implements GrantEntity, Activatable { @@ -60,6 +63,7 @@ public final class SebClientConfig implements GrantEntity, Activatable { @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ACTIVE) public final Boolean active; + @JsonCreator public SebClientConfig( @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ID) final Long id, @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_INSTITUTION_ID) final Long institutionId, @@ -130,10 +134,12 @@ public final class SebClientConfig implements GrantEntity, Activatable { return this.date; } + @JsonIgnore public CharSequence getEncryptSecret() { return this.encryptSecret; } + @JsonIgnore public CharSequence getConfirmEncryptSecret() { return this.confirmEncryptSecret; } @@ -147,6 +153,19 @@ public final class SebClientConfig implements GrantEntity, Activatable { return this.active; } + @Override + public Entity printSecureCopy() { + return new SebClientConfig( + this.id, + this.institutionId, + this.name, + this.fallbackStartURL, + this.date, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE, + this.active); + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); @@ -160,10 +179,6 @@ public final class SebClientConfig implements GrantEntity, Activatable { builder.append(this.fallbackStartURL); builder.append(", date="); builder.append(this.date); - builder.append(", encryptSecret="); - builder.append(this.encryptSecret); - builder.append(", confirmEncryptSecret="); - builder.append(this.confirmEncryptSecret); builder.append(", active="); builder.append(this.active); builder.append("]"); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java index 9c35d17f..ba3bcc1b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java @@ -19,9 +19,6 @@ import ch.ethz.seb.sebserver.gbl.model.GrantEntity; @JsonIgnoreProperties(ignoreUnknown = true) public final class ClientConnection implements GrantEntity { - public static final String FILTER_ATTR_EXAM_ID = Domain.CLIENT_CONNECTION.ATTR_EXAM_ID; - public static final String FILTER_ATTR_STATUS = Domain.CLIENT_CONNECTION.ATTR_STATUS; - public enum ConnectionStatus { UNDEFINED, CONNECTION_REQUESTED, @@ -32,6 +29,20 @@ public final class ClientConnection implements GrantEntity { RELEASED } + public static final ClientConnection EMPTY_CLIENT_CONNECTION = new ClientConnection( + -1L, + -1L, + -1L, + ConnectionStatus.UNDEFINED, + null, + null, + null, + null); + + public static final String FILTER_ATTR_EXAM_ID = Domain.CLIENT_CONNECTION.ATTR_EXAM_ID; + public static final String FILTER_ATTR_STATUS = Domain.CLIENT_CONNECTION.ATTR_STATUS; + public static final String FILTER_ATTR_SESSION_ID = Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_IDENTIFER; + @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_ID) public final Long id; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientEvent.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientEvent.java index 045f5a1f..af8c231a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientEvent.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientEvent.java @@ -21,7 +21,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; @JsonIgnoreProperties(ignoreUnknown = true) -public final class ClientEvent implements Entity, IndicatorValueHolder { +public class ClientEvent implements Entity, IndicatorValueHolder { /** Adapt SEB API to SEB_SEB_Server API -> timestamp == clientTime */ public static final String ATTR_TIMESTAMP = "timestamp"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ExtendedClientEvent.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ExtendedClientEvent.java new file mode 100644 index 00000000..486f3918 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ExtendedClientEvent.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.model.session; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import ch.ethz.seb.sebserver.gbl.model.Domain; + +public final class ExtendedClientEvent extends ClientEvent { + + public static final String FILTER_ATTRIBUTE_EXAM = Domain.CLIENT_CONNECTION.ATTR_EXAM_ID; + + @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_INSTITUTION_ID) + public final Long institutionId; + + @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) + public final Long examId; + + @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_IDENTIFER) + public final String userSessionId; + + @JsonCreator + public ExtendedClientEvent( + @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_INSTITUTION_ID) final Long institutionId, + @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId, + @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_IDENTIFER) final String userSessionId, + @JsonProperty(Domain.CLIENT_EVENT.ATTR_ID) final Long id, + @JsonProperty(Domain.CLIENT_EVENT.ATTR_CONNECTION_ID) final Long connectionId, + @JsonProperty(Domain.CLIENT_EVENT.ATTR_TYPE) final EventType eventType, + @JsonProperty(ATTR_TIMESTAMP) final Long clientTime, + @JsonProperty(Domain.CLIENT_EVENT.ATTR_SERVER_TIME) final Long serverTime, + @JsonProperty(Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE) final Double numValue, + @JsonProperty(Domain.CLIENT_EVENT.ATTR_TEXT) final String text) { + + super(id, connectionId, eventType, clientTime, serverTime, numValue, text); + + this.institutionId = institutionId; + this.examId = examId; + this.userSessionId = userSessionId; + } + + public Long getInstitutionId() { + return this.institutionId; + } + + public Long getExamId() { + return this.examId; + } + + public String getUserSessionId() { + return this.userSessionId; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("ExtendedClientEvent [institutionId="); + builder.append(this.institutionId); + builder.append(", examId="); + builder.append(this.examId); + builder.append(", userSessionId="); + builder.append(this.userSessionId); + builder.append(", connectionId="); + builder.append(this.connectionId); + builder.append(", eventType="); + builder.append(this.eventType); + builder.append(", clientTime="); + builder.append(this.clientTime); + builder.append(", serverTime="); + builder.append(this.serverTime); + builder.append(", numValue="); + builder.append(this.numValue); + builder.append(", text="); + builder.append(this.text); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/PasswordChange.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/PasswordChange.java index 46e27a1d..08a6ec70 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/PasswordChange.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/PasswordChange.java @@ -14,6 +14,7 @@ import javax.validation.constraints.Size; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.Domain.USER; import ch.ethz.seb.sebserver.gbl.model.Entity; @@ -85,4 +86,13 @@ public class PasswordChange implements Entity { return "PasswordChange"; } + @Override + public Entity printSecureCopy() { + return new PasswordChange( + this.userId, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java index 2a2a8f0d..fc050adc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java @@ -405,5 +405,4 @@ public final class Utils { org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE); return headers; } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java index 0674ba65..f3930bd3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java @@ -303,7 +303,7 @@ public class ExamForm implements TemplateComposer { .newAction(ActionDefinition.EXAM_CONFIGURATION_MODIFY_FROM_LIST) .create()) - .compose(content); + .compose(pageContext.copyOf(content)); final EntityKey configMapKey = (configurationTable.hasAnyContent()) ? configurationTable.getFirstRowData().getEntityKey() @@ -369,7 +369,7 @@ public class ExamForm implements TemplateComposer { .withParentEntityKey(entityKey) .create()) - .compose(content); + .compose(pageContext.copyOf(content)); actionBuilder diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java index 1a898a43..f2c1aa02 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java @@ -144,13 +144,13 @@ public class ExamList implements TemplateComposer { .withColumn(new ColumnDefinition<>( Domain.EXAM.ATTR_TYPE, COLUMN_TITLE_TYPE_KEY, - this.resourceService::examTypeName) + this.resourceService::localizedExamTypeName) .withFilter(this.typeFilter) .sortable()) .withDefaultAction(actionBuilder .newAction(ActionDefinition.EXAM_VIEW_FROM_LIST) .create()) - .compose(content); + .compose(pageContext.copyOf(content)); // propagate content actions to action-pane final GrantCheck userGrant = currentUser.grantCheck(EntityType.EXAM); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/InstitutionList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/InstitutionList.java index dc4c4939..9d7cd82f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/InstitutionList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/InstitutionList.java @@ -100,7 +100,7 @@ public class InstitutionList implements TemplateComposer { .withDefaultAction(pageActionBuilder .newAction(ActionDefinition.INSTITUTION_VIEW_FROM_LIST) .create()) - .compose(content); + .compose(pageContext.copyOf(content)); // propagate content actions to action-pane final GrantCheck instGrant = this.currentUser.grantCheck(EntityType.INSTITUTION); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupList.java index ed59bf0d..12898a5b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupList.java @@ -147,7 +147,7 @@ public class LmsSetupList implements TemplateComposer { .withDefaultAction(actionBuilder .newAction(ActionDefinition.LMS_SETUP_VIEW_FROM_LIST) .create()) - .compose(content); + .compose(pageContext.copyOf(content)); // propagate content actions to action-pane final GrantCheck userGrant = currentUser.grantCheck(EntityType.LMS_SETUP); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java index 477c1f44..2eecf209 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java @@ -169,10 +169,10 @@ public class MonitoringClientConnection implements TemplateComposer { .withRestCallAdapter(restCallBuilder -> restCallBuilder.withQueryParam( ClientEvent.FILTER_ATTR_CONECTION_ID, entityKey.modelId)) - .withColumn(new ColumnDefinition<>( + .withColumn(new ColumnDefinition( Domain.CLIENT_EVENT.ATTR_TYPE, LIST_COLUMN_TYPE_KEY, - this::getEventTypeName) + this.resourceService::getEventTypeName) .withFilter(this.typeFilter) .sortable() .widthProportion(2)) @@ -202,7 +202,7 @@ public class MonitoringClientConnection implements TemplateComposer { .sortable() .widthProportion(1)) - .compose(content); + .compose(pageContext.copyOf(content)); this.pageService .pageActionBuilder( @@ -216,10 +216,6 @@ public class MonitoringClientConnection implements TemplateComposer { } - public String getEventTypeName(final ClientEvent clientEvent) { - return this.resourceService.getEventTypeName(clientEvent.eventType); - } - private final String getClientTime(final ClientEvent event) { if (event == null || event.getClientTime() == null) { return Constants.EMPTY_NOTE; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java index 9942dffa..1481e79b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java @@ -151,10 +151,6 @@ public class MonitoringRunningExam implements TemplateComposer { return pageAction; }) -// .withSelect( -// () -> clientTable.getSelection(), -// PageAction::applySingleSelection, -// EMPTY_SELECTION_TEXT_KEY) .publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER)); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExamList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExamList.java index d43f117e..bf37c254 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExamList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExamList.java @@ -104,7 +104,7 @@ public class MonitoringRunningExamList implements TemplateComposer { .withColumn(new ColumnDefinition<>( Domain.EXAM.ATTR_TYPE, COLUMN_TITLE_TYPE_KEY, - this.resourceService::examTypeName) + this.resourceService::localizedExamTypeName) .withFilter(this.typeFilter) .sortable()) .withColumn(new ColumnDefinition<>( @@ -124,7 +124,7 @@ public class MonitoringRunningExamList implements TemplateComposer { .withDefaultAction(actionBuilder .newAction(ActionDefinition.MONITOR_EXAM_FROM_LIST) .create()) - .compose(content); + .compose(pageContext.copyOf(content)); actionBuilder diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizDiscoveryList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizDiscoveryList.java index a8401af4..e4ecaa67 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizDiscoveryList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizDiscoveryList.java @@ -157,7 +157,7 @@ public class QuizDiscoveryList implements TemplateComposer { .withExec(action -> this.showDetails(action, t.getSelectedROWData())) .noEventPropagation() .create()) - .compose(content); + .compose(pageContext.copyOf(content)); // propagate content actions to action-pane final GrantCheck lmsSetupGrant = currentUser.grantCheck(EntityType.LMS_SETUP); @@ -221,7 +221,11 @@ public class QuizDiscoveryList implements TemplateComposer { } private void createDetailsForm(final QuizData quizData, final PageContext pc) { - this.pageService.formBuilder(pc, 3) + + final Composite parent = pc.getParent(); + final Composite grid = this.widgetFactory.createPopupScrollComposite(parent); + + this.pageService.formBuilder(pc.copyOf(grid), 3) .withEmptyCellSeparation(false) .readonly(true) .addField(FormBuilder.singleSelection( @@ -251,7 +255,6 @@ public class QuizDiscoveryList implements TemplateComposer { QUIZ_DETAILS_URL_TEXT_KEY, quizData.startURL)) .build(); - this.widgetFactory.labelSeparator(pc.getParent()); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigList.java index 6ad49655..4f9ac88b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigList.java @@ -151,7 +151,7 @@ public class SebClientConfigList implements TemplateComposer { .withDefaultAction(pageActionBuilder .newAction(ActionDefinition.SEB_CLIENT_CONFIG_VIEW_FROM_LIST) .create()) - .compose(content); + .compose(pageContext.copyOf(content)); final GrantCheck clientConfigGrant = this.currentUser.grantCheck(EntityType.SEB_CLIENT_CONFIGURATION); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientLogs.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientLogs.java index 2c9c4a17..d1a61994 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientLogs.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientLogs.java @@ -8,26 +8,374 @@ package ch.ethz.seb.sebserver.gui.content; +import java.util.Map; +import java.util.function.Function; + +import org.eclipse.swt.widgets.Composite; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; +import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; +import ch.ethz.seb.sebserver.gui.form.FormBuilder; +import ch.ethz.seb.sebserver.gui.service.ResourceService; +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnection; +import ch.ethz.seb.sebserver.gui.table.ColumnDefinition; +import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute; +import ch.ethz.seb.sebserver.gui.table.EntityTable; +import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; @Lazy @Component @GuiProfile public class SebClientLogs implements TemplateComposer { - public SebClientLogs() { - // TODO Auto-generated constructor stub + private static final Logger log = LoggerFactory.getLogger(SebClientLogs.class); + + private static final LocTextKey DETAILS_TITLE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.details.title"); + private static final LocTextKey TITLE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.list.title"); + private static final LocTextKey EMPTY_TEXT_KEY = + new LocTextKey("sebserver.seblogs.list.empty"); + + private static final LocTextKey EXAM_TEXT_KEY = + new LocTextKey("sebserver.seblogs.list.column.exam"); + private static final LocTextKey CLIENT_SESSION_TEXT_KEY = + new LocTextKey("sebserver.seblogs.list.column.client-session"); + private static final LocTextKey TYPE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.list.column.type"); + private static final LocTextKey TIME_TEXT_KEY = + new LocTextKey("sebserver.seblogs.list.column.time"); + private static final LocTextKey VALUE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.list.column.value"); + + private static final LocTextKey DETAILS_EVENT_TILE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.details.event.title"); + private static final LocTextKey DETAILS_CONNECTION_TILE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.details.connection.title"); + private static final LocTextKey DETAILS_EXAM_TILE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.details.exam.title"); + + private static final LocTextKey FORM_TYPE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.type"); + private static final LocTextKey FORM_SERVERTIME_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.server-time"); + private static final LocTextKey FORM_CLIENTTIME_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.client-time"); + private static final LocTextKey FORM_VALUE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.value"); + private static final LocTextKey FORM_MESSAGE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.message"); + + private static final LocTextKey FORM_SESSION_ID_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.connection.session-id"); + private static final LocTextKey FORM_ADDRESS_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.connection.address"); + private static final LocTextKey FORM_TOKEN_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.connection.token"); + private static final LocTextKey FORM_STATUS_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.connection.status"); + + private static final LocTextKey FORM_EXAM_NAME_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.exam.name"); + private static final LocTextKey FORM_DESC_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.exam.description"); + private static final LocTextKey FORM_EXAM_TYPE_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.exam.type"); + private static final LocTextKey FORM_START_TIME_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.exam.startTime"); + private static final LocTextKey FORM_END_TIME_TEXT_KEY = + new LocTextKey("sebserver.seblogs.form.column.exam.endTime"); + + private final static LocTextKey EMPTY_SELECTION_TEXT = + new LocTextKey("sebserver.seblogs.info.pleaseSelect"); + + private final TableFilterAttribute examFilter; + private final TableFilterAttribute clientSessionFilter; + private final TableFilterAttribute eventTypeFilter; + + private final PageService pageService; + private final ResourceService resourceService; + private final RestService restService; + private final I18nSupport i18nSupport; + private final WidgetFactory widgetFactory; + private final int pageSize; + + public SebClientLogs( + final PageService pageService, + @Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) { + + this.pageService = pageService; + this.resourceService = pageService.getResourceService(); + this.restService = this.resourceService.getRestService(); + this.i18nSupport = this.resourceService.getI18nSupport(); + this.widgetFactory = pageService.getWidgetFactory(); + this.pageSize = pageSize; + + this.examFilter = new TableFilterAttribute( + CriteriaType.SINGLE_SELECTION, + ExtendedClientEvent.FILTER_ATTRIBUTE_EXAM, + this.resourceService::getExamResources); + + this.clientSessionFilter = new TableFilterAttribute( + CriteriaType.TEXT, + ClientConnection.FILTER_ATTR_SESSION_ID); + + this.eventTypeFilter = new TableFilterAttribute( + CriteriaType.SINGLE_SELECTION, + ClientEvent.FILTER_ATTR_TYPE, + this.resourceService::clientEventTypeResources); } @Override public void compose(final PageContext pageContext) { - // TODO Auto-generated method stub + final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); + final RestService restService = this.resourceService.getRestService(); + // content page layout with title + final Composite content = widgetFactory.defaultPageLayout( + pageContext.getParent(), + TITLE_TEXT_KEY); + + final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder( + pageContext + .clearEntityKeys() + .clearAttributes()); + + // table + final EntityTable table = this.pageService.entityTableBuilder( + restService.getRestCall(GetExtendedClientEventPage.class)) + .withEmptyMessage(EMPTY_TEXT_KEY) + .withPaging(this.pageSize) + + .withColumn(new ColumnDefinition<>( + Domain.CLIENT_CONNECTION.ATTR_EXAM_ID, + EXAM_TEXT_KEY, + examNameFunction()) + .withFilter(this.examFilter) + .widthProportion(2)) + + .withColumn(new ColumnDefinition<>( + Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_IDENTIFER, + CLIENT_SESSION_TEXT_KEY, + ExtendedClientEvent::getUserSessionId) + .withFilter(this.clientSessionFilter) + .sortable() + .widthProportion(2)) + + .withColumn(new ColumnDefinition( + Domain.CLIENT_EVENT.TYPE_NAME, + TYPE_TEXT_KEY, + this.resourceService::getEventTypeName) + .withFilter(this.eventTypeFilter) + .sortable() + .widthProportion(1)) + + .withColumn(new ColumnDefinition<>( + Domain.CLIENT_EVENT.ATTR_SERVER_TIME, + TIME_TEXT_KEY, + this::getEventTime) + .withFilter(new TableFilterAttribute( + CriteriaType.DATE_RANGE, + ClientEvent.FILTER_ATTR_SERVER_TIME_FROM_TO, + Utils.toDateTimeUTC(Utils.getMillisecondsNow()) + .minusYears(1) + .toString())) + .sortable() + .widthProportion(2)) + + .withColumn(new ColumnDefinition( + Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE, + VALUE_TEXT_KEY, + clientEvent -> (clientEvent.numValue != null) + ? String.valueOf(clientEvent.numValue) + : Constants.EMPTY_NOTE) + .widthProportion(1)) + + .withDefaultAction(t -> actionBuilder + .newAction(ActionDefinition.LOGS_SEB_CLIENT_SHOW_DETAILS) + .withExec(action -> this.showDetails(action, t.getSelectedROWData())) + .noEventPropagation() + .create()) + + .compose(pageContext.copyOf(content)); + + actionBuilder + .newAction(ActionDefinition.LOGS_SEB_CLIENT_SHOW_DETAILS) + .withSelect( + table::getSelection, + action -> this.showDetails(action, table.getSelectedROWData()), + EMPTY_SELECTION_TEXT) + .noEventPropagation() + .publishIf(table::hasAnyContent); + } + + private PageAction showDetails(final PageAction action, final ExtendedClientEvent clientEvent) { + action.getSingleSelection(); + + final ModalInputDialog dialog = new ModalInputDialog<>( + action.pageContext().getParent().getShell(), + this.widgetFactory); + dialog.setDialogWidth(600); + + dialog.open( + DETAILS_TITLE_TEXT_KEY, + action.pageContext(), + pc -> createDetailsForm(clientEvent, pc)); + + return action; + } + + private void createDetailsForm(final ExtendedClientEvent clientEvent, final PageContext pc) { + + final Composite parent = pc.getParent(); + final Composite content = this.widgetFactory.createPopupScrollComposite(parent); + + // Event Details Title + this.widgetFactory.labelLocalized( + content, + CustomVariant.TEXT_H3, + DETAILS_EVENT_TILE_TEXT_KEY); + + this.pageService.formBuilder(pc.copyOf(content), 3) + .withEmptyCellSeparation(false) + .readonly(true) + .addField(FormBuilder.text( + Domain.CLIENT_EVENT.TYPE_NAME, + FORM_TYPE_TEXT_KEY, + this.resourceService.getEventTypeName(clientEvent))) + .addField(FormBuilder.text( + Domain.CLIENT_EVENT.ATTR_CLIENT_TIME, + FORM_CLIENTTIME_TEXT_KEY, + this.i18nSupport.formatDisplayDateTime(clientEvent.clientTime))) + .addField(FormBuilder.text( + Domain.CLIENT_EVENT.ATTR_SERVER_TIME, + FORM_SERVERTIME_TEXT_KEY, + this.i18nSupport.formatDisplayDateTime(clientEvent.serverTime))) + .addField(FormBuilder.text( + Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE, + FORM_VALUE_TEXT_KEY, + (clientEvent.numValue != null) + ? String.valueOf(clientEvent.numValue) + : Constants.EMPTY_NOTE)) + .addField(FormBuilder.text( + Domain.CLIENT_EVENT.ATTR_TEXT, + FORM_MESSAGE_TEXT_KEY, + clientEvent.text) + .asArea()) + .build(); + + // SEB Client Connection Title + this.widgetFactory.labelLocalized( + content, + CustomVariant.TEXT_H3, + DETAILS_CONNECTION_TILE_TEXT_KEY); + + final ClientConnection connection = this.restService.getBuilder(GetClientConnection.class) + .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(clientEvent.connectionId)) + .call() + .get( + error -> log.error("Failed to get Exam for id {}", clientEvent.examId, error), + () -> ClientConnection.EMPTY_CLIENT_CONNECTION); + + this.pageService.formBuilder(pc.copyOf(content), 3) + .withEmptyCellSeparation(false) + .readonly(true) + .addField(FormBuilder.text( + Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_IDENTIFER, + FORM_SESSION_ID_TEXT_KEY, + connection.userSessionId)) + .addField(FormBuilder.text( + Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS, + FORM_ADDRESS_TEXT_KEY, + connection.clientAddress)) + .addField(FormBuilder.text( + Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN, + FORM_TOKEN_TEXT_KEY, + connection.connectionToken)) + .addField(FormBuilder.text( + Domain.CLIENT_CONNECTION.ATTR_STATUS, + FORM_STATUS_TEXT_KEY, + this.resourceService.localizedClientConnectionStatusName(connection.status))) + .build(); + + // Exam Details Title + this.widgetFactory.labelLocalized( + content, + CustomVariant.TEXT_H3, + DETAILS_EXAM_TILE_TEXT_KEY); + + final Exam exam = this.restService.getBuilder(GetExam.class) + .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(clientEvent.examId)) + .call() + .get( + error -> log.error("Failed to get Exam for id {}", clientEvent.examId, error), + () -> Exam.EMPTY_EXAM); + + this.pageService.formBuilder(pc.copyOf(content), 3) + .withEmptyCellSeparation(false) + .readonly(true) + .addField(FormBuilder.text( + QuizData.QUIZ_ATTR_NAME, + FORM_EXAM_NAME_TEXT_KEY, + exam.name)) + .addField(FormBuilder.text( + QuizData.QUIZ_ATTR_DESCRIPTION, + FORM_DESC_TEXT_KEY, + exam.description)) + .addField(FormBuilder.text( + Domain.EXAM.ATTR_TYPE, + FORM_EXAM_TYPE_TEXT_KEY, + this.resourceService.localizedExamTypeName(exam))) + .addField(FormBuilder.text( + QuizData.QUIZ_ATTR_START_TIME, + FORM_START_TIME_TEXT_KEY, + this.i18nSupport.formatDisplayDateTime(exam.startTime))) + .addField(FormBuilder.text( + QuizData.QUIZ_ATTR_END_TIME, + FORM_END_TIME_TEXT_KEY, + this.i18nSupport.formatDisplayDateTime(exam.endTime))) + .build(); } + private Function examNameFunction() { + final Map examNameMapping = this.resourceService.getExamNameMapping(); + return event -> examNameMapping.get(event.examId); + } + + private final String getEventTime(final ExtendedClientEvent event) { + if (event == null || event.serverTime == null) { + return Constants.EMPTY_NOTE; + } + + return this.i18nSupport + .formatDisplayDateTime(Utils.toDateTimeUTC(event.serverTime)); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigList.java index cb652054..03613e3e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigList.java @@ -137,7 +137,7 @@ public class SebExamConfigList implements TemplateComposer { .withDefaultAction(pageActionBuilder .newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP_FROM_LIST) .create()) - .compose(content); + .compose(pageContext.copyOf(content)); final GrantCheck examConfigGrant = this.currentUser.grantCheck(EntityType.CONFIGURATION_NODE); pageActionBuilder diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/UserAccountList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/UserAccountList.java index ace73423..94ec5f28 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/UserAccountList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/UserAccountList.java @@ -179,7 +179,7 @@ public class UserAccountList implements TemplateComposer { .withDefaultAction(actionBuilder .newAction(ActionDefinition.USER_ACCOUNT_VIEW_FROM_LIST) .create()) - .compose(content); + .compose(pageContext.copyOf(content)); // propagate content actions to action-pane final GrantCheck userGrant = currentUser.grantCheck(EntityType.USER); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/UserActivityLogs.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/UserActivityLogs.java index 4e968a77..0f3264d7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/UserActivityLogs.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/UserActivityLogs.java @@ -30,7 +30,7 @@ import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.userlogs.GetUserLogPage; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetUserLogPage; import ch.ethz.seb.sebserver.gui.table.ColumnDefinition; import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute; import ch.ethz.seb.sebserver.gui.table.EntityTable; @@ -54,8 +54,10 @@ public class UserActivityLogs implements TemplateComposer { new LocTextKey("sebserver.userlogs.list.column.dateTime"); private static final LocTextKey ACTIVITY_TEXT_KEY = new LocTextKey("sebserver.userlogs.list.column.activityType"); - private static final LocTextKey ENTITY_TEXT_KEY = + private static final LocTextKey ENTITY_TYPE_TEXT_KEY = new LocTextKey("sebserver.userlogs.list.column.entityType"); + private static final LocTextKey ENTITY_ID_TEXT_KEY = + new LocTextKey("sebserver.userlogs.list.column.entityId"); private static final LocTextKey MESSAGE_TEXT_KEY = new LocTextKey("sebserver.userlogs.list.column.message"); private final static LocTextKey EMPTY_SELECTION_TEXT = @@ -73,12 +75,11 @@ public class UserActivityLogs implements TemplateComposer { public UserActivityLogs( final PageService pageService, - final ResourceService resourceService, @Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) { this.pageService = pageService; - this.resourceService = resourceService; - this.i18nSupport = resourceService.getI18nSupport(); + this.resourceService = pageService.getResourceService(); + this.i18nSupport = this.resourceService.getI18nSupport(); this.widgetFactory = pageService.getWidgetFactory(); this.pageSize = pageSize; @@ -133,7 +134,7 @@ public class UserActivityLogs implements TemplateComposer { .withColumn(new ColumnDefinition( Domain.USER_ACTIVITY_LOG.ATTR_ENTITY_ID, - ENTITY_TEXT_KEY, + ENTITY_TYPE_TEXT_KEY, this.resourceService::getEntityTypeName) .withFilter(this.entityFilter) .sortable()) @@ -156,7 +157,7 @@ public class UserActivityLogs implements TemplateComposer { .noEventPropagation() .create()) - .compose(content); + .compose(pageContext.copyOf(content)); actionBuilder .newAction(ActionDefinition.LOGS_USER_ACTIVITY_SHOW_DETAILS) @@ -185,6 +186,7 @@ public class UserActivityLogs implements TemplateComposer { action.pageContext().getParent().getShell(), this.widgetFactory); + //dialog.setDialogHeight(400); dialog.open( DETAILS_TITLE_TEXT_KEY, action.pageContext(), @@ -194,7 +196,11 @@ public class UserActivityLogs implements TemplateComposer { } private void createDetailsForm(final UserActivityLog userActivityLog, final PageContext pc) { - this.pageService.formBuilder(pc, 3) + + final Composite parent = pc.getParent(); + final Composite grid = this.widgetFactory.createPopupScrollComposite(parent); + + this.pageService.formBuilder(pc.copyOf(grid), 3) .withEmptyCellSeparation(false) .readonly(true) .addField(FormBuilder.text( @@ -206,9 +212,13 @@ public class UserActivityLogs implements TemplateComposer { ACTIVITY_TEXT_KEY, this.resourceService.getUserActivityTypeName(userActivityLog))) .addField(FormBuilder.text( - Domain.USER_ACTIVITY_LOG.ATTR_ENTITY_ID, - ENTITY_TEXT_KEY, + Domain.USER_ACTIVITY_LOG.ATTR_ENTITY_TYPE, + ENTITY_TYPE_TEXT_KEY, this.resourceService.getEntityTypeName(userActivityLog))) + .addField(FormBuilder.text( + Domain.USER_ACTIVITY_LOG.ATTR_ENTITY_ID, + ENTITY_ID_TEXT_KEY, + userActivityLog.entityId)) .addField(FormBuilder.text( Domain.USER_ACTIVITY_LOG.ATTR_TIMESTAMP, DATE_TEXT_KEY, @@ -217,10 +227,9 @@ public class UserActivityLogs implements TemplateComposer { .addField(FormBuilder.text( Domain.USER_ACTIVITY_LOG.ATTR_MESSAGE, MESSAGE_TEXT_KEY, - String.valueOf(userActivityLog.message).replace(",", ",\n")) + String.valueOf(userActivityLog.message)) .asArea()) .build(); - this.widgetFactory.labelSeparator(pc.getParent()); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivitiesPane.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivitiesPane.java index 245678f1..14b86c00 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivitiesPane.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivitiesPane.java @@ -235,10 +235,9 @@ public class ActivitiesPane implements TemplateComposer { final boolean viewUserActivityLogs = this.currentUser.hasInstitutionalPrivilege( PrivilegeType.READ, EntityType.USER_ACTIVITY_LOG); - final boolean viewSebClientLogs = false; -// this.currentUser.hasInstitutionalPrivilege( -// PrivilegeType.READ, -// EntityType.EXAM); + final boolean viewSebClientLogs = this.currentUser.hasInstitutionalPrivilege( + PrivilegeType.READ, + EntityType.EXAM); TreeItem logRoot = null; if (viewUserActivityLogs && viewSebClientLogs) { @@ -384,6 +383,9 @@ public class ActivitiesPane implements TemplateComposer { } static final TreeItem findItemByActionDefinition(final TreeItem[] items, final PageState pageState) { + if (pageState == null) { + return null; + } return findItemByActionDefinition(items, pageState.activityAnchor(), null); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java index 0ce2c519..38fdf9e2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java @@ -200,6 +200,11 @@ public class FormBuilder { return new TextFieldBuilder(name, label, value); } + public static TextFieldBuilder text(final String name, final LocTextKey label, + final Supplier valueSupplier) { + return new TextFieldBuilder(name, label, valueSupplier.get()); + } + public static SelectionFieldBuilder singleSelection( final String name, final LocTextKey label, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java index 8cfac3c2..7ea46cf5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java @@ -63,9 +63,9 @@ public final class TextFieldBuilder extends FieldBuilder { ? builder.widgetFactory.textAreaInput(fieldGrid) : builder.widgetFactory.textInput(fieldGrid, this.isPassword); - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); - if (this.isArea && !readonly) { - gridData.heightHint = 50; + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + if (this.isArea) { + gridData.minimumHeight = 35; } textInput.setLayoutData(gridData); if (StringUtils.isNoneBlank(this.value)) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java index ec75298b..f8892869 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -37,6 +38,8 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; @@ -49,6 +52,7 @@ import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingNames; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.GetUserAccountNames; @@ -75,6 +79,9 @@ public class ResourceService { EntityType.USER_ROLE, EntityType.WEBSERVICE_SERVER_INFO); + public static final EnumSet CLIENT_EVENT_TYPE_EXCLUDE_MAP = EnumSet.of( + EventType.LAST_PING); + public static final String EXAMCONFIG_STATUS_PREFIX = "sebserver.examconfig.status."; public static final String EXAM_TYPE_PREFIX = "sebserver.exam.type."; public static final String USERACCOUNT_ROLE_PREFIX = "sebserver.useraccount.role."; @@ -83,6 +90,7 @@ public class ResourceService { public static final String CLIENT_EVENT_TYPE_PREFIX = "sebserver.monitoring.exam.connection.event.type."; public static final String USER_ACTIVITY_TYPE_PREFIX = "sebserver.overall.types.activityType."; public static final String ENTITY_TYPE_PREFIX = "sebserver.overall.types.entityType."; + public static final String SEB_CONNECTION_STATUS_KEY_PREFIX = "sebserver.monitoring.exam.connection.status."; public static final LocTextKey ACTIVE_TEXT_KEY = new LocTextKey("sebserver.overall.status.active"); public static final LocTextKey INACTIVE_TEXT_KEY = new LocTextKey("sebserver.overall.status.inactive"); @@ -138,6 +146,7 @@ public class ResourceService { public List> clientEventTypeResources() { return Arrays.asList(EventType.values()) .stream() + .filter(Predicate.not(CLIENT_EVENT_TYPE_EXCLUDE_MAP::contains)) .map(eventType -> new Tuple<>( eventType.name(), getEventTypeName(eventType))) @@ -145,6 +154,13 @@ public class ResourceService { .collect(Collectors.toList()); } + public String getEventTypeName(final ClientEvent event) { + if (event == null) { + return getEventTypeName(EventType.UNKNOWN); + } + return getEventTypeName(event.eventType); + } + public String getEventTypeName(final EventType eventType) { if (eventType == null) { return Constants.EMPTY_NOTE; @@ -384,6 +400,51 @@ public class ResourceService { .getText(ResourceService.EXAMCONFIG_STATUS_PREFIX + config.configStatus.name()); } + public String localizedClientConnectionStatusName(final ConnectionStatus status) { + String name; + if (status != null) { + name = status.name(); + } else { + name = ConnectionStatus.UNDEFINED.name(); + } + return this.i18nSupport + .getText(SEB_CONNECTION_STATUS_KEY_PREFIX + name, name); + } + + public String localizedExamTypeName(final Exam exam) { + if (exam.type == null) { + return Constants.EMPTY_NOTE; + } + + return this.i18nSupport + .getText(ResourceService.EXAM_TYPE_PREFIX + exam.type.name()); + } + + public List> getExamResources() { + final UserInfo userInfo = this.currentUser.get(); + return this.restService.getBuilder(GetExamNames.class) + .withQueryParam(Entity.FILTER_ATTR_INSTITUTION, String.valueOf(userInfo.getInstitutionId())) + .call() + .getOr(Collections.emptyList()) + .stream() + .map(entityName -> new Tuple<>(entityName.modelId, entityName.name)) + .sorted(RESOURCE_COMPARATOR) + .collect(Collectors.toList()); + } + + public Map getExamNameMapping() { + final UserInfo userInfo = this.currentUser.get(); + return this.restService.getBuilder(GetExamNames.class) + .withQueryParam(Entity.FILTER_ATTR_INSTITUTION, String.valueOf(userInfo.getInstitutionId())) + .call() + .getOr(Collections.emptyList()) + .stream() + .filter(k -> StringUtils.isNotBlank(k.modelId)) + .collect(Collectors.toMap( + k -> Long.valueOf(k.modelId), + k -> k.name)); + } + private Result> getExamConfigurationSelection() { return this.restService.getBuilder(GetExamConfigMappingNames.class) .withQueryParam( @@ -398,13 +459,4 @@ public class ResourceService { .call(); } - public String examTypeName(final Exam exam) { - if (exam.type == null) { - return Constants.EMPTY_NOTE; - } - - return this.i18nSupport - .getText(ResourceService.EXAM_TYPE_PREFIX + exam.type.name()); - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java index 54c59655..1929165d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java @@ -13,6 +13,8 @@ import java.util.Locale; import org.joda.time.DateTime; +import ch.ethz.seb.sebserver.gbl.util.Utils; + public interface I18nSupport { /** Get all supported languages as a collection of Locale @@ -38,6 +40,18 @@ public interface I18nSupport { * @return date formatted date String to display */ String formatDisplayDate(DateTime date); + /** Format a time-stamp (milliseconds) to a text format to display. + * This uses the date-format defined by either the attribute 'sebserver.gui.date.displayformat' + * or the Constants.DEFAULT_DISPLAY_DATE_FORMAT + * + * Adds time-zone information if the currents user time-zone is different form UTC + * + * @param date the DateTime instance + * @return date formatted date String to display */ + default String formatDisplayDate(final Long timestamp) { + return formatDisplayDate(Utils.toDateTimeUTC(timestamp)); + } + /** Format a DateTime to a text format to display. * This uses the date-format defined by either the attribute 'sebserver.gui.datetime.displayformat' * or the Constants.DEFAULT_DISPLAY_DATE_TIME_FORMAT @@ -48,6 +62,18 @@ public interface I18nSupport { * @return date formatted date time String to display */ String formatDisplayDateTime(DateTime date); + /** Format a time-stamp (milliseconds) to a text format to display. + * This uses the date-format defined by either the attribute 'sebserver.gui.datetime.displayformat' + * or the Constants.DEFAULT_DISPLAY_DATE_TIME_FORMAT + * + * Adds time-zone information if the currents user time-zone is different form UTC + * + * @param date the DateTime instance + * @return date formatted date time String to display */ + default String formatDisplayDateTime(final Long timestamp) { + return formatDisplayDateTime(Utils.toDateTimeUTC(timestamp)); + } + /** Format a DateTime to a text format to display. * This uses the date-format defined by either the attribute 'sebserver.gui.time.displayformat' * or the Constants.DEFAULT_DISPLAY_TIME_FORMAT @@ -58,6 +84,18 @@ public interface I18nSupport { * @return date formatted time String to display */ String formatDisplayTime(DateTime date); + /** Format a time-stamp (milliseconds) to a text format to display. + * This uses the date-format defined by either the attribute 'sebserver.gui.time.displayformat' + * or the Constants.DEFAULT_DISPLAY_TIME_FORMAT + * + * Adds time-zone information if the currents user time-zone is different form UTC + * + * @param date the DateTime instance + * @return date formatted time String to display */ + default String formatDisplayTime(final Long timestamp) { + return formatDisplayTime(Utils.toDateTimeUTC(timestamp)); + } + /** If the current user has another time zone then UTC this will return a tile suffix that describes * a date/time column title with adding (UTC|{usersTimeZone}) that can be added to the title. * diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java index 9e05da1a..6f384782 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java @@ -121,14 +121,14 @@ public interface PageService { * @param pageContext the PageContext on that the FormBuilder should work * @param rows the number of rows of the from * @return a FormBuilder instance for the given PageContext and with number of rows */ - FormBuilder formBuilder(final PageContext pageContext, final int rows); + FormBuilder formBuilder(PageContext pageContext, int rows); /** Get an new TableBuilder for specified page based RestCall. * * @param apiCall the SEB Server API RestCall that feeds the table with data * @param the type of the Entity of the table * @return TableBuilder of specified type */ - TableBuilder entityTableBuilder(final RestCall> apiCall); + TableBuilder entityTableBuilder(RestCall> apiCall); /** Get a new PageActionBuilder for a given PageContext. * diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java index d88b41f0..cddfb5ea 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java @@ -13,7 +13,6 @@ import java.util.function.Supplier; import org.eclipse.rap.rwt.RWT; import org.eclipse.swt.SWT; -import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; @@ -42,6 +41,7 @@ public class ModalInputDialog extends Dialog { private final WidgetFactory widgetFactory; private int dialogWidth = 400; private int dialogHeight = 600; + private final int buttonWidth = 100; public ModalInputDialog( final Shell parent, @@ -75,25 +75,21 @@ public class ModalInputDialog extends Dialog { shell.setText(this.widgetFactory.getI18nSupport().getText(title)); shell.setLayout(new GridLayout(2, true)); final GridData gridData2 = new GridData(SWT.FILL, SWT.TOP, false, false); - gridData2.widthHint = this.dialogWidth; - //gridData2.heightHint = this.dialogHeight; shell.setLayoutData(gridData2); final Composite main = new Composite(shell, SWT.NONE); main.setLayout(new GridLayout()); - final GridData gridData = new GridData(SWT.FILL, SWT.TOP, false, false); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); gridData.horizontalSpan = 2; gridData.widthHint = this.dialogWidth; main.setLayoutData(gridData); final Supplier valueSuppier = contentComposer.compose(main); - - final Point computeSize = main.computeSize(SWT.DEFAULT, SWT.DEFAULT); - gridData.heightHint = (computeSize.y < this.dialogHeight) ? computeSize.y : this.dialogHeight; + gridData.heightHint = calcDialogHeight(main); final Button ok = this.widgetFactory.buttonLocalized(shell, OK_TEXT_KEY); GridData data = new GridData(GridData.HORIZONTAL_ALIGN_END); - data.widthHint = 100; + data.widthHint = this.buttonWidth; ok.setLayoutData(data); ok.addListener(SWT.Selection, event -> { if (valueSuppier != null) { @@ -109,7 +105,7 @@ public class ModalInputDialog extends Dialog { final Button cancel = this.widgetFactory.buttonLocalized(shell, CANCEL_TEXT_KEY); data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); - data.widthHint = 100; + data.widthHint = this.buttonWidth; cancel.setLayoutData(data); cancel.addListener(SWT.Selection, event -> { if (cancelCallback != null) { @@ -132,18 +128,22 @@ public class ModalInputDialog extends Dialog { shell.setData(RWT.CUSTOM_VARIANT, CustomVariant.MESSAGE.key); shell.setText(this.widgetFactory.getI18nSupport().getText(title)); shell.setLayout(new GridLayout()); - shell.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + final GridData gridData2 = new GridData(SWT.FILL, SWT.TOP, true, true); + + shell.setLayoutData(gridData2); final Composite main = new Composite(shell, SWT.NONE); main.setLayout(new GridLayout()); final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true); + gridData.widthHint = this.dialogWidth; main.setLayoutData(gridData); - final PageContext internalPageContext = pageContext.copyOf(main); - contentComposer.accept(internalPageContext); + + contentComposer.accept(pageContext.copyOf(main)); + gridData.heightHint = calcDialogHeight(main); final Button close = this.widgetFactory.buttonLocalized(shell, CLOSE_TEXT_KEY); final GridData data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); - data.widthHint = 100; + data.widthHint = this.buttonWidth; close.setLayoutData(data); close.addListener(SWT.Selection, event -> { shell.close(); @@ -164,4 +164,16 @@ public class ModalInputDialog extends Dialog { shell.open(); } + private int calcDialogHeight(final Composite main) { + final int actualHeight = main.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; + final int displayHeight = main.getDisplay().getClientArea().height; + final int availableHeight = (displayHeight < actualHeight + 100) + ? displayHeight - 100 + : actualHeight; + final int height = (availableHeight > this.dialogHeight) + ? this.dialogHeight + : availableHeight; + return height; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitution.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitution.java index d869c1fc..609ced7c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitution.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitution.java @@ -34,7 +34,8 @@ public class GetInstitution extends RestCall { }), HttpMethod.GET, MediaType.APPLICATION_FORM_URLENCODED, - API.INSTITUTION_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT); + API.INSTITUTION_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/logs/GetExtendedClientEventPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/logs/GetExtendedClientEventPage.java new file mode 100644 index 00000000..8bc4d36c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/logs/GetExtendedClientEventPage.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Page; +import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetExtendedClientEventPage extends RestCall> { + + public GetExtendedClientEventPage() { + super(new TypeKey<>( + CallType.GET_PAGE, + EntityType.CLIENT_EVENT, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.SEB_CLIENT_EVENT_EXTENDED_PAGE_ENDPOINT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/userlogs/GetUserLogPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/logs/GetUserLogPage.java similarity index 96% rename from src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/userlogs/GetUserLogPage.java rename to src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/logs/GetUserLogPage.java index a01b8f5f..7be35ce3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/userlogs/GetUserLogPage.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/logs/GetUserLogPage.java @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.userlogs; +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpMethod; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnection.java new file mode 100644 index 00000000..741cd022 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnection.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetClientConnection extends RestCall { + + public GetClientConnection() { + super(new TypeKey<>( + CallType.GET_SINGLE, + EntityType.CLIENT_CONNECTION, + new TypeReference() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.SEB_CLIENT_CONNECTION_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionDataList.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionDataList.java index 0006f830..b1bb5b6e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionDataList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionDataList.java @@ -36,7 +36,8 @@ public class GetClientConnectionDataList extends RestCall { public final String columnName; public final String initValue; public final Supplier>> resourceSupplier; + public final Function, List>> resourceFunction; public TableFilterAttribute(final CriteriaType type, final String columnName) { - this(type, columnName, "", null); + this(type, columnName, "", (Supplier>>) null); } public TableFilterAttribute( @@ -140,7 +141,7 @@ public final class ColumnDefinition { final String columnName, final String initValue) { - this(type, columnName, initValue, null); + this(type, columnName, initValue, (Supplier>>) null); } public TableFilterAttribute( @@ -153,6 +154,20 @@ public final class ColumnDefinition { this.columnName = columnName; this.initValue = initValue; this.resourceSupplier = resourceSupplier; + this.resourceFunction = null; + } + + public TableFilterAttribute( + final CriteriaType type, + final String columnName, + final String initValue, + final Function, List>> resourceFunction) { + + this.type = type; + this.columnName = columnName; + this.initValue = initValue; + this.resourceSupplier = null; + this.resourceFunction = resourceFunction; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java index ec2198ce..75359eef 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java @@ -46,6 +46,7 @@ import ch.ethz.seb.sebserver.gbl.model.PageSortOrder; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageMessageException; import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; @@ -66,6 +67,7 @@ public class EntityTable { final RestCall> restCall; final Function>.RestCallBuilder, RestCall>.RestCallBuilder> restCallAdapter; final I18nSupport i18nSupport; + final PageContext pageContext; final List> columns; final LocTextKey emptyMessage; @@ -84,7 +86,7 @@ public class EntityTable { EntityTable( final int type, - final Composite parent, + final PageContext pageContext, final RestCall> restCall, final Function>.RestCallBuilder, RestCall>.RestCallBuilder> restCallAdapter, final PageService pageService, @@ -94,9 +96,10 @@ public class EntityTable { final Function, PageAction> defaultActionFunction, final boolean hideNavigation) { - this.composite = new Composite(parent, type); + this.composite = new Composite(pageContext.getParent(), type); this.pageService = pageService; this.i18nSupport = pageService.getI18nSupport(); + this.pageContext = pageContext; this.widgetFactory = pageService.getWidgetFactory(); this.restCall = restCall; this.restCallAdapter = (restCallAdapter != null) ? restCallAdapter : Function.identity(); @@ -365,9 +368,7 @@ public class EntityTable { .call() .map(this::createTableRowsFromPage) .map(this.navigator::update) - .onError(t -> { - // TODO error handling - }); + .onError(this.pageContext::notifyError); this.composite.getParent().layout(true, true); PageService.updateScrolledComposite(this.composite); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java index 5002d15f..fc54bf02 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java @@ -15,11 +15,11 @@ import java.util.function.Function; import java.util.function.Supplier; import org.eclipse.swt.SWT; -import org.eclipse.swt.widgets.Composite; import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; @@ -106,10 +106,10 @@ public class TableBuilder { return this; } - public EntityTable compose(final Composite parent) { + public EntityTable compose(final PageContext pageContext) { return new EntityTable<>( this.type, - parent, + pageContext, this.restCall, this.restCallAdapter, this.pageService, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java index 928c2746..7787d4ae 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gui.table; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -29,6 +30,7 @@ import org.springframework.util.MultiValueMap; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.model.Entity; +import ch.ethz.seb.sebserver.gbl.util.Tuple; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute; import ch.ethz.seb.sebserver.gui.widget.Selection; @@ -302,11 +304,17 @@ public class TableFilter { final Composite innerComposite = createInnerComposite(parent); final GridData gridData = new GridData(SWT.FILL, SWT.END, true, true); + Supplier>> resourceSupplier = this.attribute.resourceSupplier; + if (this.attribute.resourceFunction != null) { + resourceSupplier = () -> this.attribute.resourceFunction.apply(TableFilter.this.entityTable); + } + this.selector = TableFilter.this.entityTable.widgetFactory .selectionLocalized( ch.ethz.seb.sebserver.gui.widget.Selection.Type.SINGLE, innerComposite, - this.attribute.resourceSupplier); + resourceSupplier); + this.selector .adaptToControl() .setLayoutData(gridData); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index 722620e7..e0c82b01 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -47,6 +47,7 @@ import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; +import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.push.ServerPushService; @Lazy @@ -196,6 +197,23 @@ public class WidgetFactory { return grid; } + /** Use this to create a scrolled Composite for usual popup forms + * + * @param parent The parent Composite + * @return the scrolled Composite to add the form content */ + public Composite createPopupScrollComposite(final Composite parent) { + final Composite grid = PageService.createManagedVScrolledComposite( + parent, + scrolledComposite -> { + final Composite g = new Composite(scrolledComposite, SWT.NONE); + g.setLayout(new GridLayout()); + g.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + return g; + }, + false); + return grid; + } + public Button buttonLocalized(final Composite parent, final String locTextKey) { final Button button = new Button(parent, SWT.NONE); this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/ClientEventExtentionMapper.java b/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/ClientEventExtentionMapper.java index 38cd1813..08186891 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/ClientEventExtentionMapper.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/ClientEventExtentionMapper.java @@ -8,8 +8,16 @@ package ch.ethz.seb.sebserver.webservice.datalayer.batis; +import static org.mybatis.dynamic.sql.SqlBuilder.equalTo; + +import java.util.Collection; + +import org.apache.ibatis.annotations.Arg; +import org.apache.ibatis.annotations.ConstructorArgs; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.ResultType; import org.apache.ibatis.annotations.SelectProvider; +import org.apache.ibatis.type.JdbcType; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.SqlBuilder; import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter; @@ -18,6 +26,7 @@ import org.mybatis.dynamic.sql.select.SelectDSL; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; import org.mybatis.dynamic.sql.util.SqlProviderAdapter; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; @Mapper @@ -36,4 +45,86 @@ public interface ClientEventExtentionMapper { .from(ClientEventRecordDynamicSqlSupport.clientEventRecord); } + @SelectProvider(type = SqlProviderAdapter.class, method = "select") + @ResultType(ClientEventExtentionMapper.ConnectionEventJoinRecord.class) + @ConstructorArgs({ + @Arg(column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT, id = true), + @Arg(column = "connection_id", javaType = Long.class, jdbcType = JdbcType.BIGINT), + @Arg(column = "type", javaType = Integer.class, jdbcType = JdbcType.INTEGER), + @Arg(column = "client_time", javaType = Long.class, jdbcType = JdbcType.BIGINT), + @Arg(column = "server_time", javaType = Long.class, jdbcType = JdbcType.BIGINT), + @Arg(column = "numeric_value", javaType = Double.class, jdbcType = JdbcType.DECIMAL), + @Arg(column = "text", javaType = String.class, jdbcType = JdbcType.VARCHAR), + + @Arg(column = "institution_id", javaType = Long.class, jdbcType = JdbcType.BIGINT), + @Arg(column = "exam_id", javaType = Long.class, jdbcType = JdbcType.BIGINT), + @Arg(column = "exam_user_session_identifer", javaType = String.class, jdbcType = JdbcType.VARCHAR) + }) + Collection selectMany(SelectStatementProvider select); + + default QueryExpressionDSL>>.JoinSpecificationFinisher selectByExample() { + return SelectDSL.selectWithMapper( + this::selectMany, + + ClientEventRecordDynamicSqlSupport.id, + ClientEventRecordDynamicSqlSupport.connectionId.as("connection_id"), + ClientEventRecordDynamicSqlSupport.type, + ClientEventRecordDynamicSqlSupport.clientTime.as("client_time"), + ClientEventRecordDynamicSqlSupport.serverTime.as("server_time"), + ClientEventRecordDynamicSqlSupport.numericValue.as("numeric_value"), + ClientEventRecordDynamicSqlSupport.text, + + ClientConnectionRecordDynamicSqlSupport.institutionId.as("institution_id"), + ClientConnectionRecordDynamicSqlSupport.examId.as("exam_id"), + ClientConnectionRecordDynamicSqlSupport.examUserSessionIdentifer.as("exam_user_session_identifer")) + + .from(ClientEventRecordDynamicSqlSupport.clientEventRecord) + + .leftJoin(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord) + .on( + ClientEventRecordDynamicSqlSupport.clientEventRecord.connectionId, + equalTo(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord.id)); + } + + public static final class ConnectionEventJoinRecord { + + public final Long id; + public final Long connection_id; + public final Integer type; + public final Long client_time; + public final Long server_time; + public final Double numeric_value; + public final String text; + + public final Long institution_id; + public final Long exam_id; + public final String exam_user_session_identifer; + + protected ConnectionEventJoinRecord( + final Long id, + final Long connection_id, + final Integer type, + final Long client_time, + final Long server_time, + final Double numeric_value, + final String text, + + final Long institution_id, + final Long exam_id, + final String exam_user_session_identifer) { + + this.id = id; + this.connection_id = connection_id; + this.type = type; + this.client_time = client_time; + this.server_time = server_time; + this.numeric_value = numeric_value; + this.text = text; + + this.institution_id = institution_id; + this.exam_id = exam_id; + this.exam_user_session_identifer = exam_user_session_identifer; + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/PaginationService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/PaginationService.java index cfafb0a5..6bad52a1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/PaginationService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/PaginationService.java @@ -37,7 +37,7 @@ public interface PaginationService { void setDefaultLimit(); - void setDefaultLimit(final String sort, final SqlTable table); + void setDefaultLimit(final String sort, final String tableName); int getPageNumber(final Integer pageNumber); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/PaginationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/PaginationServiceImpl.java index 71d4792c..716b3ca0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/PaginationServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/PaginationServiceImpl.java @@ -94,8 +94,8 @@ public class PaginationServiceImpl implements PaginationService { } @Override - public void setDefaultLimit(final String sort, final SqlTable table) { - setPagination(1, this.maxPageSize, sort, table); + public void setDefaultLimit(final String sort, final String tableName) { + setPagination(1, this.maxPageSize, sort, tableName); } @Override @@ -131,9 +131,9 @@ public class PaginationServiceImpl implements PaginationService { final Supplier>> delegate) { return Result.tryCatch(() -> { - final SqlTable table = SqlTable.of(tableName); + //final SqlTable table = SqlTable.of(tableName); final com.github.pagehelper.Page page = - setPagination(pageNumber, pageSize, sort, table); + setPagination(pageNumber, pageSize, sort, tableName); final Collection list = delegate.get().getOrThrow(); @@ -145,36 +145,36 @@ public class PaginationServiceImpl implements PaginationService { }); } - private String verifySortColumnName(final String sort, final SqlTable table) { + private String verifySortColumnName(final String sort, final String columnName) { if (StringUtils.isBlank(sort)) { - return this.defaultSortColumn.get(table.name()); + return this.defaultSortColumn.get(columnName); } - final Map mapping = this.sortColumnMapping.get(table.name()); + final Map mapping = this.sortColumnMapping.get(columnName); if (mapping != null) { final String sortColumn = PageSortOrder.decode(sort); if (StringUtils.isBlank(sortColumn)) { - return this.defaultSortColumn.get(table.name()); + return this.defaultSortColumn.get(columnName); } return mapping.get(sortColumn); } - return this.defaultSortColumn.get(table.name()); + return this.defaultSortColumn.get(columnName); } private com.github.pagehelper.Page setPagination( final Integer pageNumber, final Integer pageSize, final String sort, - final SqlTable table) { + final String sortMappingName) { final com.github.pagehelper.Page startPage = PageHelper.startPage(getPageNumber(pageNumber), getPageSize(pageSize), true, true, false); - if (table != null && StringUtils.isNotBlank(sort)) { + if (StringUtils.isNotBlank(sortMappingName) && StringUtils.isNotBlank(sort)) { final PageSortOrder sortOrder = PageSortOrder.getSortOrder(sort); - final String sortColumnName = verifySortColumnName(sort, table); + final String sortColumnName = verifySortColumnName(sort, sortMappingName); if (StringUtils.isNotBlank(sortColumnName)) { switch (sortOrder) { case DESCENDING: { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationService.java index 68ad08bf..77e1350c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationService.java @@ -8,7 +8,10 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.authorization; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Set; import ch.ethz.seb.sebserver.gbl.api.EntityType; @@ -233,17 +236,21 @@ public interface AuthorizationService { /** Checks if the current user has a specified role. * If not a PermissionDeniedException is thrown for the given EntityType * - * @param role The UserRole to check + * @param roles The UserRoles to check if any of them matches to current users roles * @param institution the institution identifier * @param type EntityType for PermissionDeniedException * @throws PermissionDeniedException if current user don't have the specified UserRole */ - default void checkRole(final UserRole role, final Long institution, final EntityType type) { + default void checkRole(final Long institution, final EntityType type, final UserRole... roles) { final SEBServerUser currentUser = this .getUserService() .getCurrentUser(); + final List rolesList = (roles == null) + ? Collections.emptyList() + : Arrays.asList(roles); + if (!currentUser.institutionId().equals(institution) || - !currentUser.getUserRoles().contains(role)) { + Collections.disjoint(currentUser.getUserRoles(), rolesList)) { throw new PermissionDeniedException( type, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionServiceImpl.java index 818ea13b..7a5e75dd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionServiceImpl.java @@ -19,7 +19,10 @@ import java.util.stream.Collectors; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.core.JsonProcessingException; + import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType; @@ -34,11 +37,13 @@ public class BulkActionServiceImpl implements BulkActionService { private final Map> supporter; private final UserActivityLogDAO userActivityLogDAO; private final ApplicationEventPublisher applicationEventPublisher; + private final JSONMapper jsonMapper; public BulkActionServiceImpl( final Collection> supporter, final UserActivityLogDAO userActivityLogDAO, - final ApplicationEventPublisher applicationEventPublisher) { + final ApplicationEventPublisher applicationEventPublisher, + final JSONMapper jsonMapper) { this.supporter = new HashMap<>(); for (final BulkActionSupportDAO support : supporter) { @@ -46,6 +51,7 @@ public class BulkActionServiceImpl implements BulkActionService { } this.userActivityLogDAO = userActivityLogDAO; this.applicationEventPublisher = applicationEventPublisher; + this.jsonMapper = jsonMapper; } @Override @@ -123,11 +129,12 @@ public class BulkActionServiceImpl implements BulkActionService { } for (final EntityKey key : action.dependencies) { + this.userActivityLogDAO.log( activityType, key.entityType, key.modelId, - "bulk action dependency"); + "Bulk Action - Dependency : " + toLogMessage(key)); } for (final EntityKey key : action.sources) { @@ -135,10 +142,22 @@ public class BulkActionServiceImpl implements BulkActionService { activityType, key.entityType, key.modelId, - "bulk action source"); + "Bulk Action - Source : " + toLogMessage(key)); } } + private String toLogMessage(final EntityKey key) { + String entityAsString; + try { + entityAsString = this.jsonMapper + .writerWithDefaultPrettyPrinter() + .writeValueAsString(key); + } catch (final JsonProcessingException e) { + entityAsString = key.toString(); + } + return entityAsString; + } + private List> getDependancySupporter(final BulkAction action) { switch (action.type) { case ACTIVATE: diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientEventDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientEventDAO.java index f4942554..ca97ca9c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientEventDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientEventDAO.java @@ -8,8 +8,17 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; +import java.util.Collection; +import java.util.function.Predicate; + import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; +import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; +import ch.ethz.seb.sebserver.gbl.util.Result; public interface ClientEventDAO extends EntityDAO { + Result> allMatchingExtended( + FilterMap filterMap, + Predicate predicate); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java index bc053102..a7307c90 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java @@ -88,8 +88,8 @@ public interface EntityDAO { .getOrThrow() .stream() .map(entity -> new EntityName( - entity.entityType(), entity.getModelId(), + entity.entityType(), entity.getName())) .collect(Collectors.toList()); }); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/FilterMap.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/FilterMap.java index acadc63e..9306af3c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/FilterMap.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/FilterMap.java @@ -30,6 +30,7 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; +import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.util.Utils; @@ -235,14 +236,14 @@ public class FilterMap extends POSTMapper { false); } - public Long getUserLogFrom(final String filterAttrFrom) { + public Long getUserLogFrom() { return getFromToValue( UserActivityLog.FILTER_ATTR_FROM_TO, UserActivityLog.FILTER_ATTR_FROM, true); } - public Long getUserLofTo(final String filterAttrTo) { + public Long getUserLofTo() { return getFromToValue( UserActivityLog.FILTER_ATTR_FROM_TO, UserActivityLog.FILTER_ATTR_TO, @@ -253,6 +254,10 @@ public class FilterMap extends POSTMapper { return getSQLWildcard(ClientEvent.FILTER_ATTR_TEXT); } + public Long getClientEventExamId() { + return getLong(ExtendedClientEvent.FILTER_ATTRIBUTE_EXAM); + } + private Long getFromToValue(final String nameCombi, final String name, final boolean from) { final Long value = getFromToValue(nameCombi, from); if (value != null) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java index 0d7901cd..30a2a44a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java @@ -29,6 +29,10 @@ public interface UserActivityLogDAO extends * @return Result of the Entity or referring to an Error id happened */ public Result logCreate(E entity); + public Result logSaveToHistory(E entity); + + public Result logUndo(E entity); + /** Create a user activity log entry for the current user of activity type IMPORT * * @param entity the Entity @@ -47,24 +51,6 @@ public interface UserActivityLogDAO extends * @return Result of the Entity or referring to an Error id happened */ public Result logModify(E entity); - /** Create a user activity log entry for the current user of activity type ACTIVATE - * - * @param entity the Entity - * @return Result of the Entity or referring to an Error id happened */ - public Result logActivate(E entity); - - /** Create a user activity log entry for the current user of activity type DEACTIVATE - * - * @param entity the Entity - * @return Result of the Entity or referring to an Error id happened */ - public Result logDeactivate(E entity); - - /** Create a user activity log entry for the current user of activity type DELETE - * - * @param entity the Entity - * @return Result of the Entity or referring to an Error id happened */ - public Result logDelete(E entity); - /** Creates a user activity log entry for the current user. * * @param activityType the activity type diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java index 035e7ab6..80280705 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java @@ -25,10 +25,15 @@ import org.springframework.transaction.annotation.Transactional; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; +import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtentionMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtentionMapper.ConnectionEventJoinRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; @@ -44,9 +49,14 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; public class ClientEventDAOImpl implements ClientEventDAO { private final ClientEventRecordMapper clientEventRecordMapper; + private final ClientEventExtentionMapper clientEventExtentionMapper; + + protected ClientEventDAOImpl( + final ClientEventRecordMapper clientEventRecordMapper, + final ClientEventExtentionMapper clientEventExtentionMapper) { - protected ClientEventDAOImpl(final ClientEventRecordMapper clientEventRecordMapper) { this.clientEventRecordMapper = clientEventRecordMapper; + this.clientEventExtentionMapper = clientEventExtentionMapper; } @Override @@ -100,7 +110,54 @@ public class ClientEventDAOImpl implements ClientEventDAO { .flatMap(DAOLoggingSupport::logAndSkipOnError) .filter(predicate) .collect(Collectors.toList())); + } + @Override + public Result> allMatchingExtended( + final FilterMap filterMap, + final Predicate predicate) { + + return Result.tryCatch(() -> this.clientEventExtentionMapper.selectByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.institutionId, + isEqualToWhenPresent(filterMap.getInstitutionId())) + .and( + ClientConnectionRecordDynamicSqlSupport.examId, + isEqualToWhenPresent(filterMap.getClientEventExamId())) + .and( + ClientConnectionRecordDynamicSqlSupport.examUserSessionIdentifer, + SqlBuilder.isLikeWhenPresent(filterMap.getSQLWildcard(ClientConnection.FILTER_ATTR_SESSION_ID))) + .and( + ClientEventRecordDynamicSqlSupport.connectionId, + isEqualToWhenPresent(filterMap.getClientEventConnectionId())) + .and( + ClientEventRecordDynamicSqlSupport.type, + isEqualToWhenPresent(filterMap.getClientEventTypeId())) + .and( + ClientEventRecordDynamicSqlSupport.type, + SqlBuilder.isNotEqualTo(EventType.LAST_PING.id)) + .and( + ClientEventRecordDynamicSqlSupport.clientTime, + SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeFrom())) + .and( + ClientEventRecordDynamicSqlSupport.clientTime, + SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeTo())) + .and( + ClientEventRecordDynamicSqlSupport.serverTime, + SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeFrom())) + .and( + ClientEventRecordDynamicSqlSupport.serverTime, + SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeTo())) + .and( + ClientEventRecordDynamicSqlSupport.text, + SqlBuilder.isLikeWhenPresent(filterMap.getClientEventText())) + .build() + .execute() + .stream() + .map(ClientEventDAOImpl::toDomainModelExtended) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .filter(predicate) + .collect(Collectors.toList())); } @Override @@ -183,7 +240,23 @@ public class ClientEventDAOImpl implements ClientEventDAO { (numericValue != null) ? numericValue.doubleValue() : null, record.getText()); }); + } + private static Result toDomainModelExtended(final ConnectionEventJoinRecord record) { + return Result.tryCatch(() -> { + + return new ExtendedClientEvent( + record.institution_id, + record.exam_id, + record.exam_user_session_identifer, + record.id, + record.connection_id, + (record.type != null) ? EventType.byId(record.type) : EventType.UNKNOWN, + record.client_time, + record.server_time, + (record.numeric_value != null) ? record.numeric_value.doubleValue() : null, + record.text); + }); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java index 60754457..4fe8e9a8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java @@ -29,8 +29,11 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import com.fasterxml.jackson.core.JsonProcessingException; + import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; @@ -59,15 +62,18 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { private final UserActivityLogRecordMapper userLogRecordMapper; private final UserRecordMapper userRecordMapper; private final UserService userService; + private final JSONMapper jsonMapper; public UserActivityLogDAOImpl( final UserActivityLogRecordMapper userLogRecordMapper, final UserRecordMapper userRecordMapper, - final UserService userService) { + final UserService userService, + final JSONMapper jsonMapper) { this.userLogRecordMapper = userLogRecordMapper; this.userRecordMapper = userRecordMapper; this.userService = userService; + this.jsonMapper = jsonMapper; } @Override @@ -81,6 +87,18 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { return log(UserLogActivityType.CREATE, entity); } + @Override + @Transactional + public Result logSaveToHistory(final E entity) { + return log(UserLogActivityType.MODIFY, entity, "SEB Exam Configuration : Save To History"); + } + + @Override + @Transactional + public Result logUndo(final E entity) { + return log(UserLogActivityType.MODIFY, entity, "SEB Exam Configuration : Undo"); + } + @Override @Transactional public Result logImport(final E entity) { @@ -99,24 +117,6 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { return log(UserLogActivityType.MODIFY, entity); } - @Override - @Transactional - public Result logActivate(final E entity) { - return log(UserLogActivityType.ACTIVATE, entity); - } - - @Override - @Transactional - public Result logDeactivate(final E entity) { - return log(UserLogActivityType.DEACTIVATE, entity); - } - - @Override - @Transactional - public Result logDelete(final E entity) { - return log(UserLogActivityType.DELETE, entity); - } - @Override @Transactional public Result log( @@ -130,7 +130,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { @Override @Transactional public Result log(final UserLogActivityType activityType, final E entity) { - return log(this.userService.getCurrentUser(), activityType, entity, null); + return log(this.userService.getCurrentUser(), activityType, entity, toMessage(entity)); } @Override @@ -273,8 +273,8 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { return all( filterMap.getInstitutionId(), filterMap.getString(UserActivityLog.FILTER_ATTR_USER), - filterMap.getUserLogFrom(UserActivityLog.FILTER_ATTR_FROM), - filterMap.getUserLofTo(UserActivityLog.FILTER_ATTR_TO), + filterMap.getUserLogFrom(), + filterMap.getUserLofTo(), filterMap.getString(UserActivityLog.FILTER_ATTR_ACTIVITY_TYPES), filterMap.getString(UserActivityLog.FILTER_ATTR_ENTITY_TYPES), predicate); @@ -312,7 +312,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { SqlBuilder.equalTo(UserActivityLogRecordDynamicSqlSupport.userUuid)) .where( UserRecordDynamicSqlSupport.institutionId, - SqlBuilder.isEqualTo(institutionId)) + SqlBuilder.isEqualToWhenPresent(institutionId)) .and( UserActivityLogRecordDynamicSqlSupport.userUuid, SqlBuilder.isEqualToWhenPresent(userId)) @@ -480,4 +480,20 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { }); } + private String toMessage(final Entity entity) { + if (entity == null) { + return Constants.EMPTY_NOTE; + } + + String entityAsString; + try { + entityAsString = entity.getName() + " = " + this.jsonMapper + .writerWithDefaultPrettyPrinter() + .writeValueAsString(entity.printSecureCopy()); + } catch (final JsonProcessingException e) { + entityAsString = entity.toString(); + } + return entityAsString; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AsyncBatchEventSaveStrategy.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AsyncBatchEventSaveStrategy.java index dc228578..bcab0885 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AsyncBatchEventSaveStrategy.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AsyncBatchEventSaveStrategy.java @@ -142,13 +142,13 @@ public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy { .forEach(clientEventMapper::insert); return null; }); + + sqlSessionTemplate.flushStatements(); } else { sleepTime += 100; } } catch (final Exception e) { log.error("unexpected Error while trying to batch store client-events: ", e); - } finally { - sqlSessionTemplate.flushStatements(); } try { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java index cac0879f..fe384ec6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java @@ -18,7 +18,7 @@ import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.Entity; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.EntityName; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; import ch.ethz.seb.sebserver.gbl.model.GrantEntity; import ch.ethz.seb.sebserver.gbl.model.Page; @@ -136,15 +136,14 @@ public abstract class ActivatableEntityController setActive(final String modelId, final boolean active) { final EntityType entityType = this.entityDAO.entityType(); - final BulkAction bulkAction = new BulkAction( - (active) ? BulkActionType.ACTIVATE : BulkActionType.DEACTIVATE, - entityType, - new EntityKey(modelId, entityType)); return this.entityDAO.byModelId(modelId) .flatMap(this.authorization::checkWrite) .flatMap(this::validForActivation) - .flatMap(entity -> this.bulkActionService.createReport(bulkAction)); + .flatMap(entity -> this.bulkActionService.createReport(new BulkAction( + (active) ? BulkActionType.ACTIVATE : BulkActionType.DEACTIVATE, + entityType, + new EntityName(modelId, entityType, entity.getName())))); } protected Result validForActivation(final T entity) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientConnectionController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientConnectionController.java new file mode 100644 index 00000000..6e62586b --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientConnectionController.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.weblayer.api; + +import java.util.Collection; + +import org.mybatis.dynamic.sql.SqlTable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.user.UserRole; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; + +@WebServiceProfile +@RestController +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_CONNECTION_ENDPOINT) +public class ClientConnectionController extends ReadonlyEntityController { + + protected ClientConnectionController( + final AuthorizationService authorization, + final BulkActionService bulkActionService, + final ClientConnectionDAO clientConnectionDAO, + final UserActivityLogDAO userActivityLogDAO, + final PaginationService paginationService, + final BeanValidationService beanValidationService) { + + super(authorization, + bulkActionService, + clientConnectionDAO, + userActivityLogDAO, + paginationService, + beanValidationService); + } + + @Override + public Collection getDependencies(final String modelId, final BulkActionType bulkActionType) { + throw new UnsupportedOperationException(); + } + + @Override + protected SqlTable getSQLTableOfEntity() { + return ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord; + } + + @Override + protected void checkReadPrivilege(final Long institutionId) { + checkRead(institutionId); + } + + @Override + protected Result checkReadAccess(final ClientConnection entity) { + return Result.tryCatch(() -> { + checkRead(entity.institutionId); + return entity; + }); + } + + @Override + protected boolean hasReadAccess(final ClientConnection entity) { + return checkReadAccess(entity).hasValue(); + } + + private void checkRead(final Long institution) { + this.authorization.checkRole( + institution, + EntityType.CLIENT_EVENT, + UserRole.EXAM_ADMIN, + UserRole.EXAM_SUPPORTER); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java index 8e2e01ec..b5cc7a71 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java @@ -10,40 +10,44 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; import java.util.Collection; -import javax.validation.Valid; - import org.mybatis.dynamic.sql.SqlTable; +import org.springframework.http.MediaType; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; +import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; -import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; @WebServiceProfile @RestController @RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_EVENT_ENDPOINT) -public class ClientEventController extends EntityController { +public class ClientEventController extends ReadonlyEntityController { private final ClientConnectionDAO clientConnectionDAO; + private final ClientEventDAO clientEventDAO; protected ClientEventController( final AuthorizationService authorization, @@ -62,16 +66,42 @@ public class ClientEventController extends EntityController allRequestParams, final Long institutionId) { - throw new UnsupportedOperationException(); - } + @RequestMapping( + path = API.SEB_CLIENT_EVENT_SEARCH_PATH_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Page getExtendedPage( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @RequestParam(name = Page.ATTR_PAGE_NUMBER, required = false) final Integer pageNumber, + @RequestParam(name = Page.ATTR_PAGE_SIZE, required = false) final Integer pageSize, + @RequestParam(name = Page.ATTR_SORT, required = false) final String sort, + @RequestParam final MultiValueMap allRequestParams) { - @Override - public ClientEvent savePut(@Valid final ClientEvent modifyData) { - throw new UnsupportedOperationException(); + // at least current user must have read access for specified entity type within its own institution + checkReadPrivilege(institutionId); + + final FilterMap filterMap = new FilterMap(allRequestParams); + + // if current user has no read access for specified entity type within other institution + // then the current users institutionId is put as a SQL filter criteria attribute to extends query performance + if (!this.authorization.hasGrant(PrivilegeType.READ, getGrantEntityType())) { + filterMap.putIfAbsent(API.PARAM_INSTITUTION_ID, String.valueOf(institutionId)); + } + + return this.paginationService.getPage( + pageNumber, + pageSize, + sort, + getSQLTableOfEntity().name(), + () -> this.clientEventDAO.allMatchingExtended(filterMap, this::hasReadAccess)) + .getOrThrow(); } @Override @@ -79,11 +109,6 @@ public class ClientEventController extends EntityController checkModifyAccess(final ClientEvent entity) { - throw new PermissionDeniedException( - EntityType.CLIENT_EVENT, - PrivilegeType.MODIFY, - this.authorization.getUserService().getCurrentUser().uuid()); - } - - @Override - protected Result checkWriteAccess(final ClientEvent entity) { - throw new PermissionDeniedException( - EntityType.CLIENT_EVENT, - PrivilegeType.WRITE, - this.authorization.getUserService().getCurrentUser().uuid()); - } - - @Override - protected Result checkCreateAccess(final ClientEvent entity) { - throw new PermissionDeniedException( - EntityType.CLIENT_EVENT, - PrivilegeType.WRITE, - this.authorization.getUserService().getCurrentUser().uuid()); - } - @Override protected boolean hasReadAccess(final ClientEvent entity) { return true; @@ -146,9 +139,10 @@ public class ClientEventController extends EntityController { +public class ConfigurationController extends ReadonlyEntityController { private final ConfigurationDAO configurationDAO; @@ -69,6 +66,7 @@ public class ConfigurationController extends EntityController this.configurationDAO.saveToHistory(config.configurationNodeId)) + .flatMap(this.userActivityLogDAO::logSaveToHistory) .getOrThrow(); } @@ -82,6 +80,7 @@ public class ConfigurationController extends EntityController this.configurationDAO.undo(config.configurationNodeId)) + .flatMap(this.userActivityLogDAO::logUndo) .getOrThrow(); } @@ -105,21 +104,6 @@ public class ConfigurationController extends EntityController allRequestParams, final Long institutionId) { - throw new UnsupportedOperationException(); - } - - @Override - public EntityProcessingReport hardDelete(final String modelId) { - throw new UnsupportedOperationException(); - } - - @Override - protected Configuration createNew(final POSTMapper postParams) { - throw new UnsupportedOperationException(); - } - @Override protected SqlTable getSQLTableOfEntity() { return ConfigurationRecordDynamicSqlSupport.configurationRecord; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java index e97b51fe..9eac0b79 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java @@ -140,7 +140,7 @@ public class ConfigurationNodeController extends EntityController logModify(final ConfigurationValue entity) { + // Skip the modify logging for each individual ConfigurationValue + return Result.of(entity); + } + @Override protected ConfigurationValue createNew(final POSTMapper postParams) { final Long institutionId = postParams.getLong(API.PARAM_INSTITUTION_ID); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java index e62191a6..107605ab 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java @@ -279,7 +279,7 @@ public abstract class EntityController { return this.checkCreateAccess(requestModel) .flatMap(this::validForCreate) .flatMap(this.entityDAO::createNew) - .flatMap(this.userActivityLogDAO::logCreate) + .flatMap(this::logCreate) .flatMap(this::notifyCreated) .getOrThrow(); } @@ -297,7 +297,7 @@ public abstract class EntityController { return this.checkModifyAccess(modifyData) .flatMap(this::validForSave) .flatMap(this.entityDAO::save) - .flatMap(this.userActivityLogDAO::logModify) + .flatMap(this::logModify) .flatMap(this::notifySaved) .getOrThrow(); } @@ -311,15 +311,16 @@ public abstract class EntityController { method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public EntityProcessingReport hardDelete(@PathVariable final String modelId) { + final EntityType entityType = this.entityDAO.entityType(); - final BulkAction bulkAction = new BulkAction( - BulkActionType.HARD_DELETE, - entityType, - new EntityKey(modelId, entityType)); return this.entityDAO.byModelId(modelId) .flatMap(this::checkWriteAccess) - .flatMap(entity -> this.bulkActionService.createReport(bulkAction)) + .flatMap(entity -> this.bulkActionService.createReport(new BulkAction( + BulkActionType.HARD_DELETE, + entityType, + new EntityName(modelId, entityType, entity.getName())))) + .flatMap(this::logBulkAction) .getOrThrow(); } @@ -451,6 +452,33 @@ public abstract class EntityController { return this.entityDAO.entityType(); } + /** Makes a CREATE user activity log for the specified entity. + * This may be overwritten if the create user activity log should be skipped. + * + * @param entity + * @return Result of entity */ + protected Result logCreate(final T entity) { + return this.userActivityLogDAO.logCreate(entity); + } + + /** Makes a MODIFY user activity log for the specified entity. + * This may be overwritten if the create user activity log should be skipped. + * + * @param entity + * @return */ + protected Result logModify(final T entity) { + return this.userActivityLogDAO.logModify(entity); + } + + /** Makes user activity log for a bulk action. + * + * @param bulkActionReport the EntityProcessingReport + * @return Result of entity */ + protected Result logBulkAction(final EntityProcessingReport bulkActionReport) { + // TODO + return Result.of(bulkActionReport); + } + /** Implements the creation of a new entity from the post parameters given within the POSTMapper * * @param postParams contains all post parameter from request diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index 97acc846..e701b8bb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -100,9 +100,9 @@ public class ExamMonitoringController { @RequestParam final MultiValueMap allRequestParams) { this.authorization.checkRole( - UserRole.EXAM_SUPPORTER, institutionId, - EntityType.EXAM); + EntityType.EXAM, + UserRole.EXAM_SUPPORTER); final FilterMap filterMap = new FilterMap(allRequestParams); @@ -136,9 +136,9 @@ public class ExamMonitoringController { @PathVariable(name = API.EXAM_API_PARAM_EXAM_ID, required = true) final Long examId) { this.authorization.checkRole( - UserRole.EXAM_SUPPORTER, institutionId, - EntityType.EXAM); + EntityType.EXAM, + UserRole.EXAM_SUPPORTER); return this.examSessionService .getConnectionData(examId) @@ -159,9 +159,9 @@ public class ExamMonitoringController { @PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) { this.authorization.checkRole( - UserRole.EXAM_SUPPORTER, institutionId, - EntityType.EXAM); + EntityType.EXAM, + UserRole.EXAM_SUPPORTER); return this.examSessionService .getConnectionData(connectionToken) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ReadonlyEntityController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ReadonlyEntityController.java new file mode 100644 index 00000000..8d5620d3 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ReadonlyEntityController.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.weblayer.api; + +import javax.validation.Valid; + +import org.springframework.util.MultiValueMap; + +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.api.POSTMapper; +import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; +import ch.ethz.seb.sebserver.gbl.model.Entity; +import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; + +public abstract class ReadonlyEntityController extends EntityController { + + private static final String ONLY_READ_ACCESS = "Only read requests available for this entity"; + + protected ReadonlyEntityController( + final AuthorizationService authorization, + final BulkActionService bulkActionService, + final EntityDAO entityDAO, + final UserActivityLogDAO userActivityLogDAO, + final PaginationService paginationService, + final BeanValidationService beanValidationService) { + + super( + authorization, + bulkActionService, + entityDAO, + userActivityLogDAO, + paginationService, + beanValidationService); + } + + @Override + public T savePut(@Valid final T modifyData) { + throw new UnsupportedOperationException(ONLY_READ_ACCESS); + } + + @Override + public T create(final MultiValueMap allRequestParams, final Long institutionId) { + throw new UnsupportedOperationException(ONLY_READ_ACCESS); + } + + @Override + public EntityProcessingReport hardDelete(final String modelId) { + throw new UnsupportedOperationException(ONLY_READ_ACCESS); + } + + @Override + protected M createNew(final POSTMapper postParams) { + throw new UnsupportedOperationException(ONLY_READ_ACCESS); + } + + @Override + protected void checkModifyPrivilege(final Long institutionId) { + throw new PermissionDeniedException( + EntityType.CLIENT_EVENT, + PrivilegeType.MODIFY, + this.authorization.getUserService().getCurrentUser().uuid()); + } + + @Override + protected Result checkModifyAccess(final T entity) { + throw new PermissionDeniedException( + EntityType.CLIENT_EVENT, + PrivilegeType.MODIFY, + this.authorization.getUserService().getCurrentUser().uuid()); + } + + @Override + protected Result checkWriteAccess(final T entity) { + throw new PermissionDeniedException( + EntityType.CLIENT_EVENT, + PrivilegeType.WRITE, + this.authorization.getUserService().getCurrentUser().uuid()); + } + + @Override + protected Result checkCreateAccess(final M entity) { + throw new PermissionDeniedException( + EntityType.CLIENT_EVENT, + PrivilegeType.WRITE, + this.authorization.getUserService().getCurrentUser().uuid()); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java index 0336cde9..106288a5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java @@ -8,105 +8,72 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; -import org.springframework.http.MediaType; -import org.springframework.util.MultiValueMap; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.InitBinder; +import org.mybatis.dynamic.sql.SqlTable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; -import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; +import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserActivityLogRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; -import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; @WebServiceProfile @RestController @RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.USER_ACTIVITY_LOG_ENDPOINT) -public class UserActivityLogController { +public class UserActivityLogController extends ReadonlyEntityController { - private final UserActivityLogDAO userActivityLogDAO; - private final AuthorizationService authorization; - private final PaginationService paginationService; - - public UserActivityLogController( - final UserActivityLogDAO userActivityLogDAO, + protected UserActivityLogController( final AuthorizationService authorization, - final PaginationService paginationService) { + final BulkActionService bulkActionService, + final EntityDAO entityDAO, + final UserActivityLogDAO userActivityLogDAO, + final PaginationService paginationService, + final BeanValidationService beanValidationService) { - this.userActivityLogDAO = userActivityLogDAO; - this.authorization = authorization; - this.paginationService = paginationService; + super( + authorization, + bulkActionService, + entityDAO, + userActivityLogDAO, + paginationService, + beanValidationService); } - @InitBinder - public void initBinder(final WebDataBinder binder) throws Exception { - this.authorization - .getUserService() - .addUsersInstitutionDefaultPropertySupport(binder); + @Override + protected void checkReadPrivilege(final Long institutionId) { + checkRead(institutionId); } - /** Rest endpoint to get a Page UserActivityLog. - * - * GET /{api}/{entity-type-endpoint-name} - * - * GET /admin-api/v1/useractivity - * GET /admin-api/v1/useractivity?page_number=2&page_size=10&sort=-name - * GET /admin-api/v1/useractivity?name=seb&active=true - * - * @param institutionId The institution identifier of the request. - * Default is the institution identifier of the institution of the current user - * @param pageNumber the number of the page that is requested - * @param pageSize the size of the page that is requested - * @param sort the sort parameter to sort the list of entities before paging - * the sort parameter is the name of the entity-model attribute to sort with a leading '-' sign for - * descending sort order - * @param allRequestParams a MultiValueMap of all request parameter that is used for filtering - * @return Page of domain-model-entities of specified type */ - @RequestMapping( - method = RequestMethod.GET, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Page getPage( - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @RequestParam(name = Page.ATTR_PAGE_NUMBER, required = false) final Integer pageNumber, - @RequestParam(name = Page.ATTR_PAGE_SIZE, required = false) final Integer pageSize, - @RequestParam(name = Page.ATTR_SORT, required = false) final String sort, - @RequestParam final MultiValueMap allRequestParams) { - - // at least current user must have read access for specified entity type within its own institution - checkBaseReadPrivilege(institutionId); - - final FilterMap filterMap = new FilterMap(allRequestParams); - filterMap.putIfAbsent(API.PARAM_INSTITUTION_ID, String.valueOf(institutionId)); - - return this.paginationService. getPage( - pageNumber, - pageSize, - sort, - UserActivityLogRecordDynamicSqlSupport.userActivityLogRecord.name(), - () -> this.userActivityLogDAO.allMatching(filterMap)) - .getOrThrow(); + @Override + protected Result checkReadAccess(final UserActivityLog entity) { + return Result.of(entity); } - private void checkBaseReadPrivilege(final Long institutionId) { - this.authorization.check( - PrivilegeType.READ, + @Override + protected boolean hasReadAccess(final UserActivityLog entity) { + return true; + } + + @Override + protected SqlTable getSQLTableOfEntity() { + return UserActivityLogRecordDynamicSqlSupport.userActivityLogRecord; + } + + private void checkRead(final Long institution) { + this.authorization.checkRole( + institution, EntityType.USER_ACTIVITY_LOG, - institutionId); + UserRole.SEB_SERVER_ADMIN, + UserRole.INSTITUTIONAL_ADMIN); } } diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 947189bd..f5f4608b 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -25,6 +25,7 @@ + @@ -32,7 +33,7 @@ - + diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 5d462354..e0e368be 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -941,6 +941,14 @@ sebserver.monitoring.exam.connection.event.type.WARN_LOG=Warn sebserver.monitoring.exam.connection.event.type.ERROR_LOG=Error sebserver.monitoring.exam.connection.event.type.LAST_PING=Last Ping +sebserver.monitoring.exam.connection.status.UNDEFINED=Undefined +sebserver.monitoring.exam.connection.status.CONNECTION_REQUESTED=Connection Requested, +sebserver.monitoring.exam.connection.status.AUTHENTICATED=Authenticated, +sebserver.monitoring.exam.connection.status.ESTABLISHED=Established, +sebserver.monitoring.exam.connection.status.CLOSED=Closed, +sebserver.monitoring.exam.connection.status.ABORTED=Aborted, +sebserver.monitoring.exam.connection.status.RELEASED=Released + ################################ # Logs ################################ @@ -956,7 +964,8 @@ sebserver.userlogs.list.column.institution=Institution sebserver.userlogs.list.column.user=User sebserver.userlogs.list.column.dateTime=Date sebserver.userlogs.list.column.activityType=User Activity -sebserver.userlogs.list.column.entityType=Entity +sebserver.userlogs.list.column.entityType=Entity Type +sebserver.userlogs.list.column.entityId=Entity-ID sebserver.userlogs.list.column.message=Message sebserver.userlogs.details.title=User Activity Log Details @@ -969,3 +978,32 @@ sebserver.seblogs.list.title=SEB Client Logs sebserver.seblogs.list.actions=Selected Log sebserver.seblogs.list.empty=No SEB client logs has been found. Please adapt or clear the filter +sebserver.seblogs.list.column.exam=Exam +sebserver.seblogs.list.column.client-session=User Session-ID +sebserver.seblogs.list.column.type=Event Type +sebserver.seblogs.list.column.time=Event Time +sebserver.seblogs.list.column.value=Value + +sebserver.seblogs.details.title=SEB Client Log Details +sebserver.seblogs.details.event.title=Event +sebserver.seblogs.details.connection.title=SEB Connection Details +sebserver.seblogs.details.exam.title=Exam Details + +sebserver.seblogs.form.column.client-session=Session-ID +sebserver.seblogs.form.column.type=Event Type +sebserver.seblogs.form.column.server-time=Server Time +sebserver.seblogs.form.column.client-time=SEB Client Time +sebserver.seblogs.form.column.value=Value +sebserver.seblogs.form.column.message=Message + +sebserver.seblogs.form.column.connection.session-id=User Session-ID +sebserver.seblogs.form.column.connection.address=SEB Client Address +sebserver.seblogs.form.column.connection.token=SEB Connection Token +sebserver.seblogs.form.column.connection.status=Connection Status + +sebserver.seblogs.form.column.exam.name=Name +sebserver.seblogs.form.column.exam.description=Description +sebserver.seblogs.form.column.exam.type=Type +sebserver.seblogs.form.column.exam.startTime=Start Time +sebserver.seblogs.form.column.exam.endTime=End Time + diff --git a/src/main/resources/static/css/sebserver.css b/src/main/resources/static/css/sebserver.css index 85fa33eb..021bde88 100644 --- a/src/main/resources/static/css/sebserver.css +++ b/src/main/resources/static/css/sebserver.css @@ -663,7 +663,6 @@ TabItem:selected:hover:first { } - Widget-ToolTip { padding: 1px 3px 2px 3px; background-color: #82be1e; diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/model/institution/InstitutionTest.java b/src/test/java/ch/ethz/seb/sebserver/gbl/model/institution/InstitutionTest.java index 2ef2528c..72444c08 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gbl/model/institution/InstitutionTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gbl/model/institution/InstitutionTest.java @@ -58,7 +58,7 @@ public class InstitutionTest { json = jsonMapper.writeValueAsString(namesList); assertEquals( - "[{\"entityType\":\"INSTITUTION\",\"modelId\":\"1\",\"name\":\"InstOne\"},{\"entityType\":\"INSTITUTION\",\"modelId\":\"2\",\"name\":\"InstTwo\"},{\"entityType\":\"INSTITUTION\",\"modelId\":\"3\",\"name\":\"InstThree\"}]", + "[{\"modelId\":\"1\",\"entityType\":\"INSTITUTION\",\"name\":\"InstOne\"},{\"modelId\":\"2\",\"entityType\":\"INSTITUTION\",\"name\":\"InstTwo\"},{\"modelId\":\"3\",\"entityType\":\"INSTITUTION\",\"name\":\"InstThree\"}]", json); } diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/UserActivityLogAPITest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/UserActivityLogAPITest.java index 3df3849b..0e8179a6 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/UserActivityLogAPITest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/UserActivityLogAPITest.java @@ -41,12 +41,12 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { }); assertNotNull(logs); - assertTrue(2 == logs.content.size()); + assertTrue(5 == logs.content.size()); } @Test - public void getAllAsSEBAdminForUser() throws Exception { - final String token = getSebAdminAccess(); + public void getAllAsInstAdmin2ForUser() throws Exception { + final String token = getAdminInstitution2Access(); // for a user in another institution, the institution has to be defined Page logs = this.jsonMapper.readValue( this.mockMvc @@ -63,7 +63,7 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { // for a user in the same institution no institution is needed logs = this.jsonMapper.readValue( - this.mockMvc.perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?user=user2") + this.mockMvc.perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?user=user3") .header("Authorization", "Bearer " + token) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) @@ -76,7 +76,7 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { } @Test - public void getAllAsSEBAdminInTimeRange() throws Exception { + public void getAllAsInst2AdminInTimeRange() throws Exception { final DateTime zeroDate = DateTime.parse("1970-01-01T00:00:00Z", Constants.STANDARD_DATE_TIME_FORMATTER); assertEquals("0", String.valueOf(zeroDate.getMillis())); final String sec2 = zeroDate.plus(1000).toString(Constants.STANDARD_DATE_TIME_FORMATTER); @@ -84,7 +84,7 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { final String sec5 = zeroDate.plus(5000).toString(Constants.STANDARD_DATE_TIME_FORMATTER); final String sec6 = zeroDate.plus(6000).toString(Constants.STANDARD_DATE_TIME_FORMATTER); - final String token = getSebAdminAccess(); + final String token = getAdminInstitution2Access(); Page logs = this.jsonMapper.readValue( this.mockMvc.perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?institutionId=2&from=" + sec2) @@ -149,9 +149,23 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { public void getAllAsSEBAdminForActivityType() throws Exception { final String token = getSebAdminAccess(); Page logs = this.jsonMapper.readValue( - this.mockMvc.perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?activity_types=CREATE") - .header("Authorization", "Bearer " + token) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + this.mockMvc.perform( + get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?activity_types=CREATE") + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference>() { + }); + + assertNotNull(logs); + assertTrue(3 == logs.content.size()); + + logs = this.jsonMapper.readValue( + this.mockMvc.perform( + get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?institutionId=1&activity_types=CREATE") + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -164,7 +178,7 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { this.mockMvc .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT - + "?activity_types=CREATE,MODIFY") + + "?institutionId=1&activity_types=CREATE,MODIFY") .header("Authorization", "Bearer " + token) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) @@ -177,12 +191,13 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { assertTrue(2 == logs.content.size()); // for other institution (2) + final String adminInstitution2Access = getAdminInstitution2Access(); logs = this.jsonMapper.readValue( this.mockMvc .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?institutionId=2&activity_types=CREATE,MODIFY") - .header("Authorization", "Bearer " + token) + .header("Authorization", "Bearer " + adminInstitution2Access) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) @@ -199,9 +214,10 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { final String token = getSebAdminAccess(); Page logs = this.jsonMapper.readValue( this.mockMvc - .perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?entity_types=INSTITUTION") - .header("Authorization", "Bearer " + token) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) + .perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + + "?institutionId=1&entity_types=INSTITUTION") + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -214,7 +230,7 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { this.mockMvc .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT - + "?entity_types=INSTITUTION,EXAM") + + "?institutionId=1&entity_types=INSTITUTION,EXAM") .header("Authorization", "Bearer " + token) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) @@ -226,12 +242,13 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { assertNotNull(logs); assertTrue(2 == logs.content.size()); + final String adminInstitution2Access = getAdminInstitution2Access(); logs = this.jsonMapper.readValue( this.mockMvc .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?entity_types=INSTITUTION,EXAM&institutionId=2") - .header("Authorization", "Bearer " + token) + .header("Authorization", "Bearer " + adminInstitution2Access) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk())