From e5f8a995e6f08376770f8d209c1569579f6699da Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 31 Jul 2019 17:34:42 +0200 Subject: [PATCH] SEBSERV-26 user activity logs --- .../ethz/seb/sebserver/WebSecurityConfig.java | 1 + .../gbl/async/AsyncExceptionHandler.java | 31 +++ .../gbl/async/AsyncServiceSpringConfig.java | 6 + .../gbl/model/user/UserActivityLog.java | 37 ++- .../gbl/model/user/UserLogActivityType.java | 3 +- .../seb/sebserver/gui/content/ExamForm.java | 80 +++++-- .../seb/sebserver/gui/content/ExamList.java | 10 +- .../content/MonitoringClientConnection.java | 9 +- .../gui/content/QuizDiscoveryList.java | 2 +- .../sebserver/gui/content/SebClientLogs.java | 33 +++ .../gui/content/UserAccountList.java | 12 +- .../gui/content/UserActivityLogs.java | 226 ++++++++++++++++++ .../gui/content/action/ActionCategory.java | 2 + .../gui/content/action/ActionDefinition.java | 20 ++ .../gui/content/activity/ActivitiesPane.java | 135 +++++++++-- .../content/activity/ActivityDefinition.java | 5 +- .../content/activity/PageStateDefinition.java | 7 +- .../ch/ethz/seb/sebserver/gui/form/Form.java | 19 +- .../seb/sebserver/gui/form/FormBuilder.java | 5 +- .../gui/form/SelectionFieldBuilder.java | 11 +- .../sebserver/gui/form/TextFieldBuilder.java | 46 ++-- .../gui/form/ThresholdListBuilder.java | 1 + .../gui/service/ResourceService.java | 75 +++++- .../gui/service/page/PageContext.java | 4 +- .../service/page/impl/ModalInputDialog.java | 2 +- .../gui/service/page/impl/PageAction.java | 6 +- .../api/userlogs/GetUserLogPage.java | 41 ++++ .../seb/sebserver/gui/table/EntityTable.java | 6 +- .../seb/sebserver/gui/table/TableFilter.java | 101 +++++--- .../sebserver/gui/widget/ThresholdList.java | 18 +- .../sebserver/gui/widget/WidgetFactory.java | 11 +- .../client/ClientCredentialServiceImpl.java | 5 +- .../servicelayer/dao/FilterMap.java | 15 ++ .../servicelayer/dao/UserActivityLogDAO.java | 6 + .../dao/impl/UserActivityLogDAOImpl.java | 178 ++++++++++---- .../lms/impl/MockupLmsAPITemplate.java | 3 - .../sebconfig/impl/ExamConfigIO.java | 42 ++-- .../sebconfig/impl/PasswordEncryptor.java | 1 - .../impl/SebClientConfigServiceImpl.java | 11 +- .../impl/SebConfigEncryptionServiceImpl.java | 7 +- .../impl/SebExamConfigServiceImpl.java | 4 +- .../api/ConfigurationNodeController.java | 3 +- .../api/SebClientConfigController.java | 18 +- .../api/UserActivityLogController.java | 74 +++--- src/main/resources/data-demo.sql | 2 +- src/main/resources/logback-spring.xml | 2 +- src/main/resources/messages.properties | 60 ++++- src/main/resources/schema-demo.sql | 2 +- src/main/resources/schema-dev.sql | 2 +- src/main/resources/static/css/sebserver.css | 50 ++-- .../gbl/model/user/UserActivityLogTest.java | 8 +- .../integration/api/admin/UserAPITest.java | 11 +- .../api/admin/UserActivityLogAPITest.java | 59 +++-- src/test/resources/schema-test.sql | 2 +- 54 files changed, 1158 insertions(+), 372 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncExceptionHandler.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientLogs.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/content/UserActivityLogs.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/userlogs/GetUserLogPage.java diff --git a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java index bbe1d3fe..d8250180 100644 --- a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java @@ -187,6 +187,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E final String httpMethod) throws IOException { super.prepareConnection(connection, httpMethod); + super.setBufferRequestBody(false); connection.setInstanceFollowRedirects(false); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncExceptionHandler.java b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncExceptionHandler.java new file mode 100644 index 00000000..1135adba --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncExceptionHandler.java @@ -0,0 +1,31 @@ +/* + * 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.async; + +import java.lang.reflect.Method; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; + +public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(AsyncExceptionHandler.class); + + public AsyncExceptionHandler() { + // TODO Auto-generated constructor stub + } + + @Override + public void handleUncaughtException(final Throwable ex, final Method method, final Object... params) { + log.error("Unexpected error while async processing. method: {}", method, ex); + throw new RuntimeException("Unexpected error while async processing: ", ex); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncServiceSpringConfig.java b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncServiceSpringConfig.java index e95be3ad..06c597d0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncServiceSpringConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncServiceSpringConfig.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gbl.async; import java.util.concurrent.Executor; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.AsyncConfigurer; @@ -38,4 +39,9 @@ public class AsyncServiceSpringConfig implements AsyncConfigurer { return threadPoolTaskExecutor(); } + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new AsyncExceptionHandler(); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLog.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLog.java index e32cc0fa..4493f995 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLog.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLog.java @@ -9,7 +9,6 @@ package ch.ethz.seb.sebserver.gbl.model.user; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import ch.ethz.seb.sebserver.gbl.api.EntityType; @@ -18,16 +17,21 @@ import ch.ethz.seb.sebserver.gbl.model.Entity; public class UserActivityLog implements Entity { + public static final String ATTR_USER_NAME = "username"; public static final String FILTER_ATTR_USER = "user"; public static final String FILTER_ATTR_FROM = "from"; public static final String FILTER_ATTR_TO = "to"; + public static final String FILTER_ATTR_FROM_TO = "from_to"; + public static final String FILTER_ATTR_ACTIVITY_TYPES = "activity_types"; public static final String FILTER_ATTR_ENTITY_TYPES = "entity_types"; - @JsonIgnore + @JsonProperty(USER_ACTIVITY_LOG.ATTR_ID) public final Long id; @JsonProperty(USER_ACTIVITY_LOG.ATTR_USER_UUID) public final String userUUID; + @JsonProperty(ATTR_USER_NAME) + public final String username; @JsonProperty(USER_ACTIVITY_LOG.ATTR_TIMESTAMP) public final Long timestamp; @JsonProperty(USER_ACTIVITY_LOG.ATTR_ACTIVITY_TYPE) @@ -41,33 +45,18 @@ public class UserActivityLog implements Entity { @JsonCreator public UserActivityLog( + @JsonProperty(USER_ACTIVITY_LOG.ATTR_ID) final Long id, @JsonProperty(USER_ACTIVITY_LOG.ATTR_USER_UUID) final String userUUID, + @JsonProperty(ATTR_USER_NAME) final String username, @JsonProperty(USER_ACTIVITY_LOG.ATTR_TIMESTAMP) final Long timestamp, @JsonProperty(USER_ACTIVITY_LOG.ATTR_ACTIVITY_TYPE) final UserLogActivityType activityType, @JsonProperty(USER_ACTIVITY_LOG.ATTR_ENTITY_TYPE) final EntityType entityType, @JsonProperty(USER_ACTIVITY_LOG.ATTR_ENTITY_ID) final String entityId, @JsonProperty(USER_ACTIVITY_LOG.ATTR_MESSAGE) final String message) { - this.id = null; - this.userUUID = userUUID; - this.timestamp = timestamp; - this.activityType = activityType; - this.entityType = entityType; - this.entityId = entityId; - this.message = message; - } - - public UserActivityLog( - final Long id, - final String userUUID, - final Long timestamp, - final UserLogActivityType activityType, - final EntityType entityType, - final String entityId, - final String message) { - this.id = id; this.userUUID = userUUID; + this.username = username; this.timestamp = timestamp; this.activityType = activityType; this.entityType = entityType; @@ -89,13 +78,17 @@ public class UserActivityLog implements Entity { @Override public String getName() { - return getModelId(); + return this.username; } public String getUserUuid() { return this.userUUID; } + public String getUsername() { + return this.username; + } + public Long getTimestamp() { return this.timestamp; } @@ -123,6 +116,8 @@ public class UserActivityLog implements Entity { builder.append(this.id); builder.append(", userUUID="); builder.append(this.userUUID); + builder.append(", username="); + builder.append(this.username); builder.append(", timestamp="); builder.append(this.timestamp); builder.append(", activityType="); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserLogActivityType.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserLogActivityType.java index d652c9f4..f8c95388 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserLogActivityType.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserLogActivityType.java @@ -1,6 +1,6 @@ /* * 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/. @@ -12,6 +12,7 @@ package ch.ethz.seb.sebserver.gbl.model.user; public enum UserLogActivityType { CREATE, IMPORT, + EXPORT, MODIFY, PASSWORD_CHANGE, DEACTIVATE, 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 e40b7006..0674ba65 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 @@ -11,7 +11,9 @@ package ch.ethz.seb.sebserver.gui.content; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.Set; import java.util.function.BooleanSupplier; +import java.util.function.Supplier; import org.apache.commons.lang3.BooleanUtils; import org.apache.tomcat.util.buf.StringUtils; @@ -23,6 +25,7 @@ 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.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; @@ -102,6 +105,8 @@ public class ExamForm implements TemplateComposer { new LocTextKey("sebserver.exam.configuration.list.column.description"); private final static LocTextKey CONFIG_STATUS_COLUMN_KEY = new LocTextKey("sebserver.exam.configuration.list.column.status"); + private final static LocTextKey CONFIG_EMPTY_SELECTION_TEXT_KEY = + new LocTextKey("sebserver.exam.configuration.list.pleaseSelect"); private final static LocTextKey INDICATOR_LIST_TITLE_KEY = new LocTextKey("sebserver.exam.indicator.list.title"); @@ -133,7 +138,7 @@ public class ExamForm implements TemplateComposer { final EntityKey parentEntityKey = pageContext.getParentEntityKey(); final boolean readonly = pageContext.isReadonly(); final boolean importFromQuizData = BooleanUtils.toBoolean( - pageContext.getAttribute(AttributeKeys.IMPORT_FROM_QUIZZ_DATA)); + pageContext.getAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA)); // get or create model data final Exam exam = (importFromQuizData @@ -244,7 +249,7 @@ public class ExamForm implements TemplateComposer { final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(formContext .clearEntityKeys() - .removeAttribute(AttributeKeys.IMPORT_FROM_QUIZZ_DATA)); + .removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA)); // propagate content actions to action-pane actionBuilder @@ -259,7 +264,7 @@ public class ExamForm implements TemplateComposer { .newAction(ActionDefinition.EXAM_CANCEL_MODIFY) .withEntityKey(entityKey) - .withAttribute(AttributeKeys.IMPORT_FROM_QUIZZ_DATA, String.valueOf(importFromQuizData)) + .withAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA, String.valueOf(importFromQuizData)) .withExec(this::cancelModify) .publishIf(() -> !readonly); @@ -318,17 +323,18 @@ public class ExamForm implements TemplateComposer { .newAction(ActionDefinition.EXAM_CONFIGURATION_DELETE_FROM_LIST) .withEntityKey(entityKey) .withSelect( - () -> { - final ExamConfigurationMap firstRowData = configurationTable.getFirstRowData(); - if (firstRowData == null) { - return Collections.emptySet(); - } else { - return new HashSet<>(Arrays.asList(firstRowData.getEntityKey())); - } - }, + getConfigMappingSelection(configurationTable), this::deleteExamConfigMapping, - null) - .publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable); + CONFIG_EMPTY_SELECTION_TEXT_KEY) + .publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable) + + .newAction(ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY) + .withSelect( + getConfigSelection(configurationTable), + this::getExamConfigKey, + CONFIG_EMPTY_SELECTION_TEXT_KEY) + .noEventPropagation() + .publishIf(() -> userGrantCheck.r() && configurationTable.hasAnyContent()); // List of Indicators widgetFactory.labelLocalized( @@ -385,17 +391,35 @@ public class ExamForm implements TemplateComposer { indicatorTable::getSelection, this::deleteSelectedIndicator, INDICATOR_EMPTY_SELECTION_TEXT_KEY) - .publishIf(() -> modifyGrant && indicatorTable.hasAnyContent() && editable) - - .newAction(ActionDefinition.SEB_EXAM_CONFIG_GET_CONFIG_KEY) - .withEntityKey(entityKey) - .withExec(SebExamConfigPropForm.getConfigKeyFunction(this.pageService)) - .noEventPropagation() - .publishIf(() -> userGrantCheck.r()); - ; + .publishIf(() -> modifyGrant && indicatorTable.hasAnyContent() && editable); } } + private Supplier> getConfigMappingSelection( + final EntityTable configurationTable) { + return () -> { + final ExamConfigurationMap firstRowData = configurationTable.getFirstRowData(); + if (firstRowData == null) { + return Collections.emptySet(); + } else { + return new HashSet<>(Arrays.asList(firstRowData.getEntityKey())); + } + }; + } + + private Supplier> getConfigSelection(final EntityTable configurationTable) { + return () -> { + final ExamConfigurationMap firstRowData = configurationTable.getFirstRowData(); + if (firstRowData == null) { + return Collections.emptySet(); + } else { + return new HashSet<>(Arrays.asList(new EntityKey( + firstRowData.configurationNodeId, + EntityType.CONFIGURATION_NODE))); + } + }; + } + private PageAction deleteSelectedIndicator(final PageAction action) { final EntityKey indicatorKey = action.getSingleSelection(); this.resourceService.getRestService() @@ -405,6 +429,18 @@ public class ExamForm implements TemplateComposer { return action; } + private PageAction getExamConfigKey(final PageAction action) { + final EntityKey examConfigMappingKey = action.getSingleSelection(); + if (examConfigMappingKey != null) { + action.withEntityKey(examConfigMappingKey); + return SebExamConfigPropForm + .getConfigKeyFunction(this.pageService) + .apply(action); + } + + return action; + } + private PageAction deleteExamConfigMapping(final PageAction action) { final EntityKey examConfigMappingKey = action.getSingleSelection(); this.resourceService.getRestService() @@ -457,7 +493,7 @@ public class ExamForm implements TemplateComposer { private PageAction cancelModify(final PageAction action) { final boolean importFromQuizData = BooleanUtils.toBoolean( - action.pageContext().getAttribute(AttributeKeys.IMPORT_FROM_QUIZZ_DATA)); + action.pageContext().getAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA)); if (importFromQuizData) { final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(action.pageContext()); final PageAction activityHomeAction = 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 999eb7b8..1a898a43 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 @@ -23,6 +23,7 @@ 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.institution.LmsSetup; 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.service.ResourceService; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; @@ -68,8 +69,6 @@ public class ExamList implements TemplateComposer { private final TableFilterAttribute lmsFilter; private final TableFilterAttribute nameFilter = new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME); - private final TableFilterAttribute startTimeFilter = - new TableFilterAttribute(CriteriaType.DATE, QuizData.FILTER_ATTR_START_TIME); private final TableFilterAttribute typeFilter; private final PageService pageService; @@ -135,7 +134,12 @@ public class ExamList implements TemplateComposer { "sebserver.exam.list.column.starttime", i18nSupport.getUsersTimeZoneTitleSuffix()), Exam::getStartTime) - .withFilter(this.startTimeFilter) + .withFilter(new TableFilterAttribute( + CriteriaType.DATE, + QuizData.FILTER_ATTR_START_TIME, + Utils.toDateTimeUTC(Utils.getMillisecondsNow()) + .minusYears(1) + .toString())) .sortable()) .withColumn(new ColumnDefinition<>( Domain.EXAM.ATTR_TYPE, 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 a724b42c..477c1f44 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 @@ -30,6 +30,7 @@ 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.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; @@ -78,6 +79,7 @@ public class MonitoringClientConnection implements TemplateComposer { private final ServerPushService serverPushService; private final PageService pageService; private final ResourceService resourceService; + private final I18nSupport i18nSupport; private final long pollInterval; private final int pageSize; @@ -95,6 +97,7 @@ public class MonitoringClientConnection implements TemplateComposer { this.serverPushService = serverPushService; this.pageService = pageService; this.resourceService = resourceService; + this.i18nSupport = resourceService.getI18nSupport(); this.pollInterval = pollInterval; this.pageSize = pageSize; @@ -222,8 +225,7 @@ public class MonitoringClientConnection implements TemplateComposer { return Constants.EMPTY_NOTE; } - return this.pageService - .getI18nSupport() + return this.i18nSupport .formatDisplayTime(Utils.toDateTimeUTC(event.getClientTime())); } @@ -232,8 +234,7 @@ public class MonitoringClientConnection implements TemplateComposer { return Constants.EMPTY_NOTE; } - return this.pageService - .getI18nSupport() + return this.i18nSupport .formatDisplayTime(Utils.toDateTimeUTC(event.getServerTime())); } 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 85e49d05..a8401af4 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 @@ -202,7 +202,7 @@ public class QuizDiscoveryList implements TemplateComposer { return action .withEntityKey(action.getSingleSelection()) .withParentEntityKey(new EntityKey(selectedROWData.lmsSetupId, EntityType.LMS_SETUP)) - .withAttribute(AttributeKeys.IMPORT_FROM_QUIZZ_DATA, "true"); + .withAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA, "true"); } private PageAction showDetails(final PageAction action, final QuizData quizData) { 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 new file mode 100644 index 00000000..2c9c4a17 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientLogs.java @@ -0,0 +1,33 @@ +/* + * 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.content; + +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; + +@Lazy +@Component +@GuiProfile +public class SebClientLogs implements TemplateComposer { + + public SebClientLogs() { + // TODO Auto-generated constructor stub + } + + @Override + public void compose(final PageContext pageContext) { + // TODO Auto-generated method stub + + } + +} 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 a222bddd..ace73423 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 @@ -47,6 +47,8 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; public class UserAccountList implements TemplateComposer { // localized text keys + private static final LocTextKey EMPTY_TEXT_KEY = + new LocTextKey("sebserver.useraccount.list.empty"); private static final LocTextKey INSTITUTION_TEXT_KEY = new LocTextKey("sebserver.useraccount.list.column.institution"); private static final LocTextKey EMPTY_SELECTION_TEXT_KEY = @@ -121,9 +123,9 @@ public class UserAccountList implements TemplateComposer { // table final EntityTable table = this.pageService.entityTableBuilder( restService.getRestCall(GetUserAccountPage.class)) - - .withEmptyMessage(new LocTextKey("sebserver.useraccount.list.empty")) + .withEmptyMessage(EMPTY_TEXT_KEY) .withPaging(this.pageSize) + .withColumnIf( isSEBAdmin, () -> new ColumnDefinition<>( @@ -132,6 +134,7 @@ public class UserAccountList implements TemplateComposer { userInstitutionNameFunction(this.resourceService)) .withFilter(this.institutionFilter) .widthProportion(2)) + .withColumn(new ColumnDefinition<>( Domain.USER.ATTR_NAME, NAME_TEXT_KEY, @@ -139,6 +142,7 @@ public class UserAccountList implements TemplateComposer { .withFilter(this.nameFilter) .sortable() .widthProportion(2)) + .withColumn(new ColumnDefinition<>( Domain.USER.ATTR_USERNAME, USER_NAME_TEXT_KEY, @@ -146,6 +150,7 @@ public class UserAccountList implements TemplateComposer { .withFilter(this.usernameFilter) .sortable() .widthProportion(2)) + .withColumn(new ColumnDefinition<>( Domain.USER.ATTR_EMAIL, MAIL_TEXT_KEY, @@ -153,6 +158,7 @@ public class UserAccountList implements TemplateComposer { .withFilter(this.mailFilter) .sortable() .widthProportion(3)) + .withColumn(new ColumnDefinition<>( Domain.USER.ATTR_LANGUAGE, LANG_TEXT_KEY, @@ -161,6 +167,7 @@ public class UserAccountList implements TemplateComposer { .localized() .sortable() .widthProportion(1)) + .withColumn(new ColumnDefinition<>( Domain.USER.ATTR_ACTIVE, ACTIVE_TEXT_KEY, @@ -168,6 +175,7 @@ public class UserAccountList implements TemplateComposer { .sortable() .withFilter(this.activityFilter) .widthProportion(1)) + .withDefaultAction(actionBuilder .newAction(ActionDefinition.USER_ACCOUNT_VIEW_FROM_LIST) .create()) 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 new file mode 100644 index 00000000..4e968a77 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/UserActivityLogs.java @@ -0,0 +1,226 @@ +/* + * 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.content; + +import org.eclipse.swt.widgets.Composite; +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.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; +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.userlogs.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; +import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; + +@Lazy +@Component +@GuiProfile +public class UserActivityLogs implements TemplateComposer { + + private static final LocTextKey DETAILS_TITLE_TEXT_KEY = + new LocTextKey("sebserver.userlogs.details.title"); + private static final LocTextKey TITLE_TEXT_KEY = + new LocTextKey("sebserver.userlogs.list.title"); + private static final LocTextKey EMPTY_TEXT_KEY = + new LocTextKey("sebserver.userlogs.list.empty"); + private static final LocTextKey USER_TEXT_KEY = + new LocTextKey("sebserver.userlogs.list.column.user"); + private static final LocTextKey DATE_TEXT_KEY = + 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 = + new LocTextKey("sebserver.userlogs.list.column.entityType"); + private static final LocTextKey MESSAGE_TEXT_KEY = + new LocTextKey("sebserver.userlogs.list.column.message"); + private final static LocTextKey EMPTY_SELECTION_TEXT = + new LocTextKey("sebserver.userlogs.info.pleaseSelect"); + + private final TableFilterAttribute userFilter; + private final TableFilterAttribute activityFilter; + private final TableFilterAttribute entityFilter; + + private final PageService pageService; + private final ResourceService resourceService; + private final I18nSupport i18nSupport; + private final WidgetFactory widgetFactory; + private final int pageSize; + + 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.widgetFactory = pageService.getWidgetFactory(); + this.pageSize = pageSize; + + this.userFilter = new TableFilterAttribute( + CriteriaType.SINGLE_SELECTION, + UserActivityLog.FILTER_ATTR_USER, + this.resourceService::userResources); + + this.activityFilter = new TableFilterAttribute( + CriteriaType.SINGLE_SELECTION, + UserActivityLog.FILTER_ATTR_ACTIVITY_TYPES, + this.resourceService::userActivityTypeResources); + + this.entityFilter = new TableFilterAttribute( + CriteriaType.SINGLE_SELECTION, + UserActivityLog.FILTER_ATTR_ENTITY_TYPES, + this.resourceService::entityTypeResources); + } + + @Override + public void compose(final PageContext pageContext) { + 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(GetUserLogPage.class)) + .withEmptyMessage(EMPTY_TEXT_KEY) + .withPaging(this.pageSize) + + .withColumn(new ColumnDefinition<>( + UserActivityLog.ATTR_USER_NAME, + USER_TEXT_KEY, + UserActivityLog::getUsername) + .withFilter(this.userFilter)) + + .withColumn(new ColumnDefinition( + Domain.USER_ACTIVITY_LOG.ATTR_ACTIVITY_TYPE, + ACTIVITY_TEXT_KEY, + this.resourceService::getUserActivityTypeName) + .withFilter(this.activityFilter) + .sortable()) + + .withColumn(new ColumnDefinition( + Domain.USER_ACTIVITY_LOG.ATTR_ENTITY_ID, + ENTITY_TEXT_KEY, + this.resourceService::getEntityTypeName) + .withFilter(this.entityFilter) + .sortable()) + + .withColumn(new ColumnDefinition<>( + Domain.USER_ACTIVITY_LOG.ATTR_TIMESTAMP, + DATE_TEXT_KEY, + this::getLogTime) + .withFilter(new TableFilterAttribute( + CriteriaType.DATE_RANGE, + UserActivityLog.FILTER_ATTR_FROM_TO, + Utils.toDateTimeUTC(Utils.getMillisecondsNow()) + .minusYears(1) + .toString())) + .sortable()) + + .withDefaultAction(t -> actionBuilder + .newAction(ActionDefinition.LOGS_USER_ACTIVITY_SHOW_DETAILS) + .withExec(action -> this.showDetails(action, t.getSelectedROWData())) + .noEventPropagation() + .create()) + + .compose(content); + + actionBuilder + .newAction(ActionDefinition.LOGS_USER_ACTIVITY_SHOW_DETAILS) + .withSelect( + table::getSelection, + action -> this.showDetails(action, table.getSelectedROWData()), + EMPTY_SELECTION_TEXT) + .noEventPropagation() + .publishIf(table::hasAnyContent); + + } + + private final String getLogTime(final UserActivityLog log) { + if (log == null || log.timestamp == null) { + return Constants.EMPTY_NOTE; + } + + return this.i18nSupport + .formatDisplayDateTime(Utils.toDateTimeUTC(log.timestamp)); + } + + private PageAction showDetails(final PageAction action, final UserActivityLog userActivityLog) { + action.getSingleSelection(); + + final ModalInputDialog dialog = new ModalInputDialog<>( + action.pageContext().getParent().getShell(), + this.widgetFactory); + + dialog.open( + DETAILS_TITLE_TEXT_KEY, + action.pageContext(), + pc -> createDetailsForm(userActivityLog, pc)); + + return action; + } + + private void createDetailsForm(final UserActivityLog userActivityLog, final PageContext pc) { + this.pageService.formBuilder(pc, 3) + .withEmptyCellSeparation(false) + .readonly(true) + .addField(FormBuilder.text( + UserActivityLog.ATTR_USER_NAME, + USER_TEXT_KEY, + String.valueOf(userActivityLog.getUsername()))) + .addField(FormBuilder.text( + Domain.USER_ACTIVITY_LOG.ATTR_ACTIVITY_TYPE, + ACTIVITY_TEXT_KEY, + this.resourceService.getUserActivityTypeName(userActivityLog))) + .addField(FormBuilder.text( + Domain.USER_ACTIVITY_LOG.ATTR_ENTITY_ID, + ENTITY_TEXT_KEY, + this.resourceService.getEntityTypeName(userActivityLog))) + .addField(FormBuilder.text( + Domain.USER_ACTIVITY_LOG.ATTR_TIMESTAMP, + DATE_TEXT_KEY, + this.widgetFactory.getI18nSupport() + .formatDisplayDateTime(Utils.toDateTimeUTC(userActivityLog.timestamp)))) + .addField(FormBuilder.text( + Domain.USER_ACTIVITY_LOG.ATTR_MESSAGE, + MESSAGE_TEXT_KEY, + String.valueOf(userActivityLog.message).replace(",", ",\n")) + .asArea()) + .build(); + this.widgetFactory.labelSeparator(pc.getParent()); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java index ab5e1e27..ae01fb1e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java @@ -23,6 +23,8 @@ public enum ActionCategory { SEB_EXAM_CONFIG_LIST(new LocTextKey("sebserver.examconfig.list.actions"), 1), RUNNING_EXAM_LIST(new LocTextKey("sebserver.monitoring.exam.list.actions"), 1), CLIENT_EVENT_LIST(new LocTextKey("sebserver.monitoring.exam.connection.list.actions"), 1), + LOGS_USER_ACTIVITY_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1), + LOGS_SEB_CLIENT_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1), VARIA(new LocTextKey("sebserver.overall.action.category.varia"), 100), ; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index 9f0680b5..6914456f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -242,6 +242,10 @@ public enum ActionDefinition { ImageIcon.DELETE, PageStateDefinition.EXAM_VIEW, ActionCategory.EXAM_CONFIG_MAPPING_LIST), + EXAM_CONFIGURATION_GET_CONFIG_KEY( + new LocTextKey("sebserver.examconfig.action.get-config-key"), + ImageIcon.SECURE, + ActionCategory.EXAM_CONFIG_MAPPING_LIST), EXAM_CONFIGURATION_SAVE( new LocTextKey("sebserver.exam.configuration.action.save"), ImageIcon.SAVE, @@ -416,6 +420,22 @@ public enum ActionDefinition { PageStateDefinition.MONITORING_RUNNING_EXAM, ActionCategory.VARIA), + LOGS_USER_ACTIVITY_LIST( + new LocTextKey("sebserver.logs.activity.userlogs"), + PageStateDefinition.USER_ACTIVITY_LOGS), + LOGS_USER_ACTIVITY_SHOW_DETAILS( + new LocTextKey("sebserver.logs.activity.userlogs.details"), + ImageIcon.SHOW, + ActionCategory.LOGS_USER_ACTIVITY_LIST), + + LOGS_SEB_CLIENT( + new LocTextKey("sebserver.logs.activity.seblogs"), + PageStateDefinition.SEB_CLIENT_LOGS), + LOGS_SEB_CLIENT_SHOW_DETAILS( + new LocTextKey("sebserver.logs.activity.seblogs.details"), + ImageIcon.SHOW, + ActionCategory.LOGS_SEB_CLIENT_LIST), + ; public final LocTextKey title; 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 e72f7078..245678f1 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 @@ -8,6 +8,7 @@ package ch.ethz.seb.sebserver.gui.content.activity; +import org.eclipse.rap.rwt.RWT; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Event; @@ -176,34 +177,46 @@ public class ActivitiesPane implements TemplateComposer { final boolean examConfigRead = this.currentUser.hasInstitutionalPrivilege( PrivilegeType.READ, EntityType.CONFIGURATION_NODE); - if (clientConfigRead || examConfigRead) { - final TreeItem sebConfigs = this.widgetFactory.treeItemLocalized( + + TreeItem sebConfigs = null; + if (clientConfigRead && examConfigRead) { + sebConfigs = this.widgetFactory.treeItemLocalized( navigation, ActivityDefinition.SEB_CONFIGURATION.displayName); + sebConfigs.setData(RWT.CUSTOM_VARIANT, CustomVariant.ACTIVITY_TREE_SECTION.key); - // SEB Client Config - if (clientConfigRead) { - final TreeItem clientConfig = this.widgetFactory.treeItemLocalized( - sebConfigs, - ActivityDefinition.SEB_CLIENT_CONFIG.displayName); - injectActivitySelection( - clientConfig, - actionBuilder - .newAction(ActionDefinition.SEB_CLIENT_CONFIG_LIST) - .create()); - } + } - // SEB Exam Config - if (examConfigRead) { - final TreeItem examConfig = this.widgetFactory.treeItemLocalized( - sebConfigs, - ActivityDefinition.SEB_EXAM_CONFIG.displayName); - injectActivitySelection( - examConfig, - actionBuilder - .newAction(ActionDefinition.SEB_EXAM_CONFIG_LIST) - .create()); - } + // SEB Client Config + if (clientConfigRead) { + final TreeItem clientConfig = (sebConfigs != null) + ? this.widgetFactory.treeItemLocalized( + sebConfigs, + ActivityDefinition.SEB_CLIENT_CONFIG.displayName) + : this.widgetFactory.treeItemLocalized( + navigation, + ActivityDefinition.SEB_CLIENT_CONFIG.displayName); + injectActivitySelection( + clientConfig, + actionBuilder + .newAction(ActionDefinition.SEB_CLIENT_CONFIG_LIST) + .create()); + } + + // SEB Exam Config + if (examConfigRead) { + final TreeItem examConfig = (sebConfigs != null) + ? this.widgetFactory.treeItemLocalized( + sebConfigs, + ActivityDefinition.SEB_EXAM_CONFIG.displayName) + : this.widgetFactory.treeItemLocalized( + navigation, + ActivityDefinition.SEB_EXAM_CONFIG.displayName); + injectActivitySelection( + examConfig, + actionBuilder + .newAction(ActionDefinition.SEB_EXAM_CONFIG_LIST) + .create()); } // Monitoring exams @@ -218,10 +231,68 @@ public class ActivitiesPane implements TemplateComposer { .create()); } + // Logs + final boolean viewUserActivityLogs = this.currentUser.hasInstitutionalPrivilege( + PrivilegeType.READ, + EntityType.USER_ACTIVITY_LOG); + final boolean viewSebClientLogs = false; +// this.currentUser.hasInstitutionalPrivilege( +// PrivilegeType.READ, +// EntityType.EXAM); + + TreeItem logRoot = null; + if (viewUserActivityLogs && viewSebClientLogs) { + logRoot = this.widgetFactory.treeItemLocalized( + navigation, + ActivityDefinition.LOGS.displayName); + logRoot.setData(RWT.CUSTOM_VARIANT, CustomVariant.ACTIVITY_TREE_SECTION.key); + } + + // User Activity Logs + if (viewUserActivityLogs) { + final TreeItem activityLogs = (logRoot != null) + ? this.widgetFactory.treeItemLocalized( + logRoot, + ActivityDefinition.USER_ACTIVITY_LOGS.displayName) + : this.widgetFactory.treeItemLocalized( + navigation, + ActivityDefinition.USER_ACTIVITY_LOGS.displayName); + injectActivitySelection( + activityLogs, + actionBuilder + .newAction(ActionDefinition.LOGS_USER_ACTIVITY_LIST) + .create()); + } + + // SEB Client Logs + if (viewSebClientLogs) { + final TreeItem sebLogs = (logRoot != null) + ? this.widgetFactory.treeItemLocalized( + logRoot, + ActivityDefinition.SEB_CLIENT_LOGS.displayName) + : this.widgetFactory.treeItemLocalized( + navigation, + ActivityDefinition.SEB_CLIENT_LOGS.displayName); + injectActivitySelection( + sebLogs, + actionBuilder + .newAction(ActionDefinition.LOGS_SEB_CLIENT) + .create()); + } + // TODO other activities // register page listener and initialize navigation data navigation.addListener(SWT.Selection, event -> handleSelection(pageContext, event)); + navigation.addListener(SWT.Expand, event -> { + final Tree tree = (Tree) event.widget; + final TreeItem treeItem = (TreeItem) event.item; + final TreeItem[] selection = tree.getSelection(); + if (selection != null && selection.length >= 1 && selection[0].getParentItem() == treeItem) { + tree.setSelection(selection[0]); + //tree.select(selection[0]); + } + }); navigation.setData( PageEventListener.LISTENER_ATTRIBUTE_KEY, new ActivitiesActionEventListener(navigation)); @@ -247,8 +318,16 @@ public class ActivitiesPane implements TemplateComposer { // if there is no form action associated with the treeItem and the treeItem has sub items, toggle the item state if (action == null) { if (treeItem.getItemCount() > 0) { - treeItem.setExpanded(!treeItem.getExpanded()); + treeItem.setExpanded(true); + final PageState currentState = this.pageService.getCurrentState(); + final TreeItem currentSelection = findItemByActionDefinition(tree.getItems(), currentState); + if (currentSelection != null) { + tree.select(currentSelection); + } else { + tree.deselectAll(); + } } + tree.layout(); return; } this.pageService.executePageAction( @@ -279,6 +358,12 @@ public class ActivitiesPane implements TemplateComposer { for (final TreeItem item : items) { final PageAction action = getActivitySelection(item); if (action == null) { + if (item.getItemCount() > 0) { + final TreeItem found = findItemByActionDefinition(item.getItems(), activity, modelId); + if (found != null) { + return found; + } + } continue; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivityDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivityDefinition.java index 81e4b281..d2f4a8a6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivityDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivityDefinition.java @@ -20,7 +20,10 @@ public enum ActivityDefinition implements Activity { SEB_CONFIGURATION(new LocTextKey("sebserver.sebconfig.activity.name")), SEB_CLIENT_CONFIG(new LocTextKey("sebserver.clientconfig.action.list")), SEB_EXAM_CONFIG(new LocTextKey("sebserver.examconfig.action.list")), - MONITORING_EXAMS(new LocTextKey("sebserver.monitoring.action.list")); + MONITORING_EXAMS(new LocTextKey("sebserver.monitoring.action.list")), + LOGS(new LocTextKey("sebserver.logs.activity.main")), + USER_ACTIVITY_LOGS(new LocTextKey("sebserver.logs.activity.userlogs")), + SEB_CLIENT_LOGS(new LocTextKey("sebserver.logs.activity.seblogs")); public final LocTextKey displayName; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinition.java index ebbfd4d6..77fb4e25 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinition.java @@ -22,12 +22,14 @@ import ch.ethz.seb.sebserver.gui.content.MonitoringRunningExamList; import ch.ethz.seb.sebserver.gui.content.QuizDiscoveryList; import ch.ethz.seb.sebserver.gui.content.SebClientConfigForm; import ch.ethz.seb.sebserver.gui.content.SebClientConfigList; +import ch.ethz.seb.sebserver.gui.content.SebClientLogs; import ch.ethz.seb.sebserver.gui.content.SebExamConfigList; import ch.ethz.seb.sebserver.gui.content.SebExamConfigPropForm; import ch.ethz.seb.sebserver.gui.content.SebExamConfigSettingsForm; import ch.ethz.seb.sebserver.gui.content.UserAccountChangePasswordForm; import ch.ethz.seb.sebserver.gui.content.UserAccountForm; import ch.ethz.seb.sebserver.gui.content.UserAccountList; +import ch.ethz.seb.sebserver.gui.content.UserActivityLogs; import ch.ethz.seb.sebserver.gui.content.action.ActionPane; import ch.ethz.seb.sebserver.gui.service.page.Activity; import ch.ethz.seb.sebserver.gui.service.page.PageState; @@ -67,7 +69,10 @@ public enum PageStateDefinition implements PageState { MONITORING_RUNNING_EXAM_LIST(Type.LIST_VIEW, MonitoringRunningExamList.class, ActivityDefinition.MONITORING_EXAMS), MONITORING_RUNNING_EXAM(Type.FORM_VIEW, MonitoringRunningExam.class, ActivityDefinition.MONITORING_EXAMS), - MONITORING_CLIENT_CONNECTION(Type.FORM_VIEW, MonitoringClientConnection.class, ActivityDefinition.MONITORING_EXAMS) + MONITORING_CLIENT_CONNECTION(Type.FORM_VIEW, MonitoringClientConnection.class, ActivityDefinition.MONITORING_EXAMS), + + USER_ACTIVITY_LOGS(Type.LIST_VIEW, UserActivityLogs.class, ActivityDefinition.USER_ACTIVITY_LOGS), + SEB_CLIENT_LOGS(Type.LIST_VIEW, SebClientLogs.class, ActivityDefinition.SEB_CLIENT_LOGS) ; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java index 4deb1045..f5bccfe6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java @@ -82,6 +82,7 @@ public final class Form implements FormBinding { .forEach(entry -> { entry.getValue() .stream() + .filter(Form::valueApplicationFilter) .forEach(ffa -> { if (ffa.listValue) { appendFormUrlEncodedValue( @@ -118,8 +119,8 @@ public final class Form implements FormBinding { return !this.formFields.isEmpty(); } - Form putField(final String name, final Label label, final Label field) { - this.formFields.add(name, createAccessor(label, field)); + Form putReadonlyField(final String name, final Label label, final Text field) { + this.formFields.add(name, createReadonlyAccessor(label, field)); return this; } @@ -225,14 +226,18 @@ public final class Form implements FormBinding { for (final Map.Entry> entry : this.formFields.entrySet()) { entry.getValue() .stream() - .filter(ffa -> StringUtils.isNotBlank(ffa.getStringValue())) + .filter(Form::valueApplicationFilter) .forEach(ffa -> ffa.putJsonValue(entry.getKey(), this.objectRoot)); } } + private static boolean valueApplicationFilter(final FormFieldAccessor ffa) { + return ffa.getStringValue() != null; + } + // following are FormFieldAccessor implementations for all field types //@formatter:off - private FormFieldAccessor createAccessor(final Label label, final Label field) { + private FormFieldAccessor createReadonlyAccessor(final Label label, final Text field) { return new FormFieldAccessor(label, field, null) { @Override public String getStringValue() { return null; } @Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); } @@ -241,7 +246,7 @@ public final class Form implements FormBinding { private FormFieldAccessor createAccessor(final Label label, final Text text, final Label errorLabel) { return new FormFieldAccessor(label, text, errorLabel) { @Override public String getStringValue() {return text.getText();} - @Override public void setStringValue(final String value) { text.setText( (value == null) ? StringUtils.EMPTY : value); } + @Override public void setStringValue(final String value) {text.setText(value);} }; } private FormFieldAccessor createAccessor(final Label label, final Selection selection, final Label errorLabel) { @@ -381,7 +386,7 @@ public final class Form implements FormBinding { this.jsonValueAdapter = jsonValueAdapter; } else { this.jsonValueAdapter = (tuple, jsonObject) -> { - if (StringUtils.isNotBlank(tuple._2)) { + if (tuple._2 != null) { jsonObject.put(tuple._1, tuple._2); } }; @@ -448,7 +453,7 @@ public final class Form implements FormBinding { gridLayout.marginRight = 0; fieldGrid.setLayout(gridLayout); - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); gridData.horizontalSpan = hspan; fieldGrid.setLayoutData(gridData); 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 0b8a889c..0ce2c519 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 @@ -68,6 +68,7 @@ public class FormBuilder { this.formParent = this.widgetFactory .formGrid(pageContext.getParent(), rows); + this.formParent.setData("TEST"); } public FormBuilder readonly(final boolean readonly) { @@ -271,7 +272,7 @@ public class FormBuilder { final GridData gridData = new GridData( (centered) ? SWT.FILL : SWT.FILL, (centered) ? SWT.CENTER : SWT.TOP, - true, true, + true, false, hspan, 1); if (centered) { @@ -279,7 +280,7 @@ public class FormBuilder { label.setData(RWT.CUSTOM_VARIANT, CustomVariant.FORM_CENTER.key); } - gridData.heightHint = FORM_ROW_HEIGHT; + // gridData.heightHint = FORM_ROW_HEIGHT; label.setLayoutData(gridData); return label; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java index 27865dcf..4aff2c70 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java @@ -21,6 +21,7 @@ import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.util.Tuple; @@ -100,7 +101,7 @@ public final class SelectionFieldBuilder extends FieldBuilder { if (this.type == Type.MULTI || this.type == Type.MULTI_COMBO) { final Composite composite = new Composite(builder.formParent, SWT.NONE); final GridLayout gridLayout = new GridLayout(1, true); - gridLayout.verticalSpacing = 0; + gridLayout.verticalSpacing = 5; gridLayout.horizontalSpacing = 0; gridLayout.marginLeft = 0; gridLayout.marginHeight = 0; @@ -120,7 +121,7 @@ public final class SelectionFieldBuilder extends FieldBuilder { .forEach(v -> buildReadonlyLabel(composite, v, 0)); } } else { - builder.form.putField( + builder.form.putReadonlyField( this.name, lab, buildReadonlyLabel(builder.formParent, this.value, this.spanInput)); @@ -128,9 +129,9 @@ public final class SelectionFieldBuilder extends FieldBuilder { } } - private Label buildReadonlyLabel(final Composite composite, final String valueKey, final int hspan) { - final Label label = new Label(composite, SWT.NONE); - final GridData gridData = new GridData(SWT.LEFT, SWT.FILL, true, true, hspan, 1); + private Text buildReadonlyLabel(final Composite composite, final String valueKey, final int hspan) { + final Text label = new Text(composite, SWT.READ_ONLY); + final GridData gridData = new GridData(SWT.LEFT, SWT.TOP, true, false, hspan, 1); gridData.verticalIndent = 0; gridData.horizontalIndent = 0; label.setLayoutData(gridData); 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 17466c8b..8cfac3c2 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 @@ -8,12 +8,14 @@ package ch.ethz.seb.sebserver.gui.form; +import org.apache.commons.lang3.StringUtils; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; public final class TextFieldBuilder extends FieldBuilder { @@ -47,37 +49,39 @@ public final class TextFieldBuilder extends FieldBuilder { return; } + final boolean readonly = builder.readonly || this.readonly; final Label lab = builder.labelLocalized( builder.formParent, this.label, this.defaultLabel, this.spanLabel); - if (builder.readonly || this.readonly) { - builder.form.putField(this.name, lab, - builder.valueLabel(builder.formParent, this.value, this.spanInput, this.centeredInput)); - builder.setFieldVisible(this.visible, this.name); + final Composite fieldGrid = Form.createFieldGrid(builder.formParent, this.spanInput); + final Text textInput = (this.isNumber) + ? builder.widgetFactory.numberInput(fieldGrid, null) + : (this.isArea) + ? 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; + } + textInput.setLayoutData(gridData); + if (StringUtils.isNoneBlank(this.value)) { + textInput.setText(this.value); + } else if (readonly) { + textInput.setText(Constants.EMPTY_NOTE); + } + + if (readonly) { + textInput.setEditable(false); + builder.form.putReadonlyField(this.name, lab, textInput); } else { - - final Composite fieldGrid = Form.createFieldGrid(builder.formParent, this.spanInput); - final Text textInput = (this.isNumber) - ? builder.widgetFactory.numberInput(fieldGrid, null) - : (this.isArea) - ? builder.widgetFactory.textAreaInput(fieldGrid) - : builder.widgetFactory.textInput(fieldGrid, this.isPassword); - - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); - if (this.isArea) { - gridData.heightHint = 50; - } - textInput.setLayoutData(gridData); - if (this.value != null) { - textInput.setText(this.value); - } - final Label errorLabel = Form.createErrorLabel(fieldGrid); builder.form.putField(this.name, lab, textInput, errorLabel); builder.setFieldVisible(this.visible, this.name); } + } } \ No newline at end of file diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/ThresholdListBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/ThresholdListBuilder.java index c2449b5c..3cbadb23 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/ThresholdListBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/ThresholdListBuilder.java @@ -49,6 +49,7 @@ public class ThresholdListBuilder extends FieldBuilder> { final Composite fieldGrid = Form.createFieldGrid(builder.formParent, this.spanInput); final ThresholdList thresholdList = builder.widgetFactory.thresholdList( fieldGrid, + fieldGrid.getParent().getParent(), this.value); final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); 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 ad9ad1fe..ec75298b 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 @@ -11,6 +11,8 @@ package ch.ethz.seb.sebserver.gui.service; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -24,6 +26,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.EntityName; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; @@ -35,7 +38,9 @@ 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.ClientEvent.EventType; +import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; +import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -56,12 +61,28 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; * combo-box content. */ public class ResourceService { + public static final Comparator> RESOURCE_COMPARATOR = (t1, t2) -> t1._2.compareTo(t2._2); + + public static final EnumSet ENTITY_TYPE_EXCLUDE_MAP = EnumSet.of( + EntityType.ADDITIONAL_ATTRIBUTES, + EntityType.CLIENT_CONNECTION, + EntityType.CLIENT_EVENT, + EntityType.CONFIGURATION_ATTRIBUTE, + EntityType.CONFIGURATION_VALUE, + EntityType.CONFIGURATION, + EntityType.ORIENTATION, + EntityType.USER_ACTIVITY_LOG, + EntityType.USER_ROLE, + EntityType.WEBSERVICE_SERVER_INFO); + 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."; public static final String EXAM_INDICATOR_TYPE_PREFIX = "sebserver.exam.indicator.type."; public static final String LMSSETUP_TYPE_PREFIX = "sebserver.lmssetup.type."; 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 LocTextKey ACTIVE_TEXT_KEY = new LocTextKey("sebserver.overall.status.active"); public static final LocTextKey INACTIVE_TEXT_KEY = new LocTextKey("sebserver.overall.status.inactive"); @@ -110,6 +131,7 @@ public class ResourceService { .map(lmsType -> new Tuple<>( lmsType.name(), this.i18nSupport.getText(LMSSETUP_TYPE_PREFIX + lmsType.name(), lmsType.name()))) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -119,6 +141,7 @@ public class ResourceService { .map(eventType -> new Tuple<>( eventType.name(), getEventTypeName(eventType))) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -135,6 +158,7 @@ public class ResourceService { .map(type -> new Tuple<>( type.name(), this.i18nSupport.getText(EXAM_INDICATOR_TYPE_PREFIX + type.name(), type.name()))) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -143,6 +167,7 @@ public class ResourceService { .getOr(Collections.emptyList()) .stream() .map(entityName -> new Tuple<>(entityName.modelId, entityName.name)) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -152,6 +177,7 @@ public class ResourceService { .map(ur -> new Tuple<>( ur.name(), this.i18nSupport.getText(USERACCOUNT_ROLE_PREFIX + ur.name()))) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -162,6 +188,7 @@ public class ResourceService { .getOr(Collections.emptyList()) .stream() .map(entityName -> new Tuple<>(entityName.modelId, entityName.name)) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -191,6 +218,7 @@ public class ResourceService { .getOr(Collections.emptyList()) .stream() .map(entityName -> new Tuple<>(entityName.modelId, entityName.name)) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -213,6 +241,45 @@ public class ResourceService { }; } + public List> entityTypeResources() { + return Arrays.asList(EntityType.values()) + .stream() + .filter(type -> !ENTITY_TYPE_EXCLUDE_MAP.contains(type)) + .map(type -> new Tuple<>(type.name(), getEntityTypeName(type))) + .sorted(RESOURCE_COMPARATOR) + .collect(Collectors.toList()); + } + + public String getEntityTypeName(final EntityType type) { + if (type == null) { + return Constants.EMPTY_NOTE; + } + return this.i18nSupport.getText(ENTITY_TYPE_PREFIX + type.name()); + } + + public String getEntityTypeName(final UserActivityLog userLog) { + return getEntityTypeName(userLog.entityType); + } + + public List> userActivityTypeResources() { + return Arrays.asList(UserLogActivityType.values()) + .stream() + .map(type -> new Tuple<>(type.name(), getUserActivityTypeName(type))) + .sorted(RESOURCE_COMPARATOR) + .collect(Collectors.toList()); + } + + public String getUserActivityTypeName(final UserLogActivityType type) { + if (type == null) { + return Constants.EMPTY_NOTE; + } + return this.i18nSupport.getText(USER_ACTIVITY_TYPE_PREFIX + type.name()); + } + + public String getUserActivityTypeName(final UserActivityLog userLog) { + return getUserActivityTypeName(userLog.activityType); + } + /** Get a list of language key/name tuples for all supported languages in the * language of the current users locale. * @@ -225,7 +292,7 @@ public class ResourceService { .stream() .map(locale -> new Tuple<>(locale.toLanguageTag(), locale.getDisplayLanguage(currentLocale))) .filter(tuple -> StringUtils.isNotBlank(tuple._2)) - .sorted((t1, t2) -> t1._2.compareTo(t2._2)) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -235,7 +302,7 @@ public class ResourceService { .getAvailableIDs() .stream() .map(id -> new Tuple<>(id, DateTimeZone.forID(id).getName(0, currentLocale) + " (" + id + ")")) - .sorted((t1, t2) -> t1._2.compareTo(t2._2)) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -246,6 +313,7 @@ public class ResourceService { .map(type -> new Tuple<>( type.name(), this.i18nSupport.getText(EXAM_TYPE_PREFIX + type.name()))) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -255,6 +323,7 @@ public class ResourceService { .map(type -> new Tuple<>( type.name(), this.i18nSupport.getText(EXAMCONFIG_STATUS_PREFIX + type.name()))) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -269,6 +338,7 @@ public class ResourceService { return selection .stream() .map(entityName -> new Tuple<>(entityName.modelId, entityName.name)) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } @@ -281,6 +351,7 @@ public class ResourceService { .getOr(Collections.emptyList()) .stream() .map(entityName -> new Tuple<>(entityName.modelId, entityName.name)) + .sorted(RESOURCE_COMPARATOR) .collect(Collectors.toList()); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java index a6ff1120..c07258fd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java @@ -36,12 +36,12 @@ public interface PageContext { public static final String ENTITY_TYPE = "ENTITY_TYPE"; public static final String PARENT_ENTITY_TYPE = "PARENT_ENTITY_TYPE"; - public static final String IMPORT_FROM_QUIZZ_DATA = "IMPORT_FROM_QUIZZ_DATA"; + public static final String IMPORT_FROM_QUIZ_DATA = "IMPORT_FROM_QUIZ_DATA"; } /** Get the I18nSupport service - * + * * @return the I18nSupport service */ I18nSupport getI18nSupport(); 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 e695dbbe..d88b41f0 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 @@ -47,7 +47,7 @@ public class ModalInputDialog extends Dialog { final Shell parent, final WidgetFactory widgetFactory) { - super(parent, SWT.BORDER | SWT.TITLE | SWT.APPLICATION_MODAL); + super(parent, SWT.BORDER | SWT.TITLE | SWT.APPLICATION_MODAL | SWT.CLOSE); this.widgetFactory = widgetFactory; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java index 8bb0bd93..4d3ba278 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java @@ -138,11 +138,7 @@ public final class PageAction { PageAction.this.pageContext.publishPageMessage(pme); return Result.ofError(pme); } catch (final RestCallError restCallError) { - if (restCallError.isFieldValidationError()) { - PageAction.this.pageContext.publishPageMessage( - new LocTextKey("sebserver.form.validation.error.title"), - new LocTextKey("sebserver.form.validation.error.message")); - } else { + if (!restCallError.isFieldValidationError()) { log.error("Failed to execute action: {}", PageAction.this, restCallError); PageAction.this.pageContext.notifyError("action.error.unexpected.message", restCallError); } 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/userlogs/GetUserLogPage.java new file mode 100644 index 00000000..a01b8f5f --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/userlogs/GetUserLogPage.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.userlogs; + +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.user.UserActivityLog; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetUserLogPage extends RestCall> { + + public GetUserLogPage() { + super(new TypeKey<>( + CallType.GET_PAGE, + EntityType.USER_ACTIVITY_LOG, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.USER_ACTIVITY_LOG_ENDPOINT); + } + +} 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 628389ea..ec2198ce 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 @@ -381,11 +381,7 @@ public class EntityTable { } final GridData gridData = (GridData) this.table.getLayoutData(); - if (page.numberOfPages > 1) { - gridData.heightHint = (this.pageSize + 1) * 27; - } else { - gridData.heightHint = (page.content.size() + 1) * 27; - } + gridData.heightHint = (page.content.size() * 25) + 40; for (final ROW row : page.content) { final TableItem item = new TableItem(this.table, SWT.NONE); 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 fbab0f28..928c2746 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 @@ -36,6 +36,9 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.ImageIcon; public class TableFilter { + private static final LocTextKey DATE_FROM_TEXT = new LocTextKey("sebserver.overall.date.from"); + private static final LocTextKey DATE_TO_TEXT = new LocTextKey("sebserver.overall.date.to"); + public static enum CriteriaType { TEXT, SINGLE_SELECTION, @@ -54,6 +57,8 @@ public class TableFilter { final RowLayout layout = new RowLayout(SWT.HORIZONTAL); layout.spacing = 5; layout.wrap = false; + layout.center = false; + layout.fill = true; this.composite.setLayout(layout); // TODO just for debugging, remove when tested @@ -135,32 +140,46 @@ public class TableFilter { } private void addActions() { - this.entityTable.widgetFactory.imageButton( + final Composite inner = new Composite(this.composite, SWT.NONE); + final GridLayout gridLayout = new GridLayout(2, true); + gridLayout.horizontalSpacing = 5; + gridLayout.verticalSpacing = 0; + gridLayout.marginHeight = 0; + gridLayout.marginWidth = 0; + inner.setLayout(gridLayout); + inner.setLayoutData(new RowData()); + + final GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, true); + + final Label imageButton = this.entityTable.widgetFactory.imageButton( ImageIcon.SEARCH, - this.composite, + inner, new LocTextKey("sebserver.overall.action.filter"), event -> { this.entityTable.applyFilter(); }); - this.entityTable.widgetFactory.imageButton( + imageButton.setLayoutData(gridData); + final Label imageButton2 = this.entityTable.widgetFactory.imageButton( ImageIcon.CANCEL, - this.composite, + inner, new LocTextKey("sebserver.overall.action.filter.clear"), event -> { reset(); this.entityTable.applyFilter(); }); + imageButton2.setLayoutData(gridData); } private static abstract class FilterComponent { - static final int CELL_WIDTH_ADJUSTMENT = -30; + static final int CELL_WIDTH_ADJUSTMENT = -5; - protected final RowData rowData = new RowData(); + protected final RowData rowData; final TableFilterAttribute attribute; FilterComponent(final TableFilterAttribute attribute) { this.attribute = attribute; + this.rowData = new RowData(); } LinkedMultiValueMap putFilterParameter( @@ -188,6 +207,18 @@ public class TableFilter { return false; } + + protected Composite createInnerComposite(final Composite parent) { + final Composite inner = new Composite(parent, SWT.NONE); + final GridLayout gridLayout = new GridLayout(1, true); + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + gridLayout.marginHeight = 0; + gridLayout.marginWidth = 0; + inner.setLayout(gridLayout); + inner.setLayoutData(this.rowData); + return inner; + } } private class NullFilter extends FilterComponent { @@ -239,8 +270,11 @@ public class TableFilter { @Override FilterComponent build(final Composite parent) { - this.textInput = TableFilter.this.entityTable.widgetFactory.textInput(parent, false); - this.textInput.setLayoutData(this.rowData); + final Composite innerComposite = createInnerComposite(parent); + final GridData gridData = new GridData(SWT.FILL, SWT.END, true, true); + + this.textInput = TableFilter.this.entityTable.widgetFactory.textInput(innerComposite); + this.textInput.setLayoutData(gridData); return this; } @@ -265,14 +299,17 @@ public class TableFilter { @Override FilterComponent build(final Composite parent) { + final Composite innerComposite = createInnerComposite(parent); + final GridData gridData = new GridData(SWT.FILL, SWT.END, true, true); + this.selector = TableFilter.this.entityTable.widgetFactory .selectionLocalized( ch.ethz.seb.sebserver.gui.widget.Selection.Type.SINGLE, - parent, + innerComposite, this.attribute.resourceSupplier); this.selector .adaptToControl() - .setLayoutData(this.rowData); + .setLayoutData(gridData); return this; } @@ -292,13 +329,6 @@ public class TableFilter { return null; } - - @Override - boolean adaptWidth(final int width) { - // NOTE: for some unknown reason RWT acts differently on width-property for text inputs and selectors - // this is to adjust selection filter criteria to the list column width - return super.adaptWidth(width + 25); - } } // NOTE: SWT DateTime month-number starting with 0 and joda DateTime with 1! @@ -312,8 +342,11 @@ public class TableFilter { @Override FilterComponent build(final Composite parent) { - this.selector = new DateTime(parent, SWT.DATE | SWT.BORDER); - this.selector.setLayoutData(this.rowData); + final Composite innerComposite = createInnerComposite(parent); + final GridData gridData = new GridData(SWT.FILL, SWT.END, true, true); + + this.selector = new DateTime(innerComposite, SWT.DATE | SWT.BORDER); + this.selector.setLayoutData(gridData); return this; } @@ -346,9 +379,9 @@ public class TableFilter { @Override boolean adaptWidth(final int width) { - // NOTE: for some unknown reason RWT acts differently on width-property for text inputs and selectors - // this is to adjust selection filter criteria to the list column width - return super.adaptWidth(width + 25); + // NOTE: for some unknown reason RWT acts differently on width-property for date selector + // this is to adjust date filter criteria to the list column width + return super.adaptWidth(width - 5); } } @@ -357,7 +390,7 @@ public class TableFilter { private class DateRange extends FilterComponent { private Composite innerComposite; - private final GridData rw1 = new GridData(); + private final GridData rw1 = new GridData(SWT.FILL, SWT.FILL, true, true); private DateTime fromSelector; private DateTime toSelector; @@ -368,15 +401,24 @@ public class TableFilter { @Override FilterComponent build(final Composite parent) { this.innerComposite = new Composite(parent, SWT.NONE); - final GridLayout gridLayout = new GridLayout(2, true); + final GridLayout gridLayout = new GridLayout(2, false); gridLayout.marginHeight = 0; gridLayout.marginWidth = 0; + gridLayout.horizontalSpacing = 5; + gridLayout.verticalSpacing = 3; this.innerComposite.setLayout(gridLayout); this.innerComposite.setLayoutData(this.rowData); + + TableFilter.this.entityTable.widgetFactory + .labelLocalized(this.innerComposite, DATE_FROM_TEXT); this.fromSelector = new DateTime(this.innerComposite, SWT.DATE | SWT.BORDER); this.fromSelector.setLayoutData(this.rw1); + + TableFilter.this.entityTable.widgetFactory + .labelLocalized(this.innerComposite, DATE_TO_TEXT); this.toSelector = new DateTime(this.innerComposite, SWT.DATE | SWT.BORDER); this.toSelector.setLayoutData(this.rw1); + return this; } @@ -405,7 +447,7 @@ public class TableFilter { .withYear(this.toSelector.getYear()) .withMonthOfYear(this.toSelector.getMonth() + 1) .withDayOfMonth(this.toSelector.getDay()) - .withTimeAtStartOfDay(); + .withTime(23, 59, 59, 0); return fromDate.toString(Constants.STANDARD_DATE_TIME_FORMATTER) + Constants.EMBEDDED_LIST_SEPARATOR + @@ -414,15 +456,6 @@ public class TableFilter { return null; } } - - @Override - boolean adaptWidth(final int width) { - // NOTE: for some unknown reason RWT acts differently on width-property for text inputs and selectors - // this is to adjust selection filter criteria to the list column width - this.rw1.widthHint = (width - 10) / 2; - return super.adaptWidth(width + 25); - - } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/ThresholdList.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/ThresholdList.java index ff36fc81..63ee7ee5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/ThresholdList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/ThresholdList.java @@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.widget.Selection.Type; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.ImageIcon; @@ -48,10 +49,21 @@ public final class ThresholdList extends Composite { private final GridData valueCell; private final GridData colorCell; private final GridData actionCell; + private final Composite updateAnchor; ThresholdList(final Composite parent, final WidgetFactory widgetFactory) { + this(parent, parent, widgetFactory); + } + + ThresholdList( + final Composite parent, + final Composite updateAnchor, + final WidgetFactory widgetFactory) { + super(parent, SWT.NONE); + this.updateAnchor = updateAnchor; this.widgetFactory = widgetFactory; + super.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); final GridLayout gridLayout = new GridLayout(3, false); gridLayout.verticalSpacing = 1; @@ -154,7 +166,8 @@ public final class ThresholdList extends Composite { final Entry entry = new Entry(valueInput, selector, imageButton); this.thresholds.add(entry); - this.getParent().layout(); + this.updateAnchor.layout(); + PageService.updateScrolledComposite(this); } private void removeThreshold(final Entry entry) { @@ -162,7 +175,8 @@ public final class ThresholdList extends Composite { entry.dispose(); } - this.getParent().layout(); + this.updateAnchor.layout(); + PageService.updateScrolledComposite(this); } private void adaptColumnWidth(final Event event) { 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 13926b3a..722620e7 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 @@ -114,11 +114,14 @@ public class WidgetFactory { TEXT_H3("h3"), IMAGE_BUTTON("imageButton"), TEXT_ACTION("action"), + TEXT_READONLY("readonlyText"), FORM_CENTER("form-center"), SELECTION("selection"), SELECTED("selected"), + ACTIVITY_TREE_SECTION("treesection"), + FOOTER("footer"), TITLE_LABEL("head"), @@ -532,8 +535,12 @@ public class WidgetFactory { return selection; } - public ThresholdList thresholdList(final Composite parent, final Collection values) { - final ThresholdList thresholdList = new ThresholdList(parent, this); + public ThresholdList thresholdList( + final Composite parent, + final Composite updateAnchor, + final Collection values) { + + final ThresholdList thresholdList = new ThresholdList(parent, updateAnchor, this); if (values != null) { thresholdList.setThresholds(values); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java index c77bc503..c6c143c2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java @@ -12,6 +12,7 @@ import java.io.UnsupportedEncodingException; import java.security.SecureRandom; import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; @@ -65,10 +66,10 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { return new ClientCredentials( clientIdPlaintext, - (secretPlaintext != null) + (StringUtils.isNoneBlank(secretPlaintext)) ? encrypt(secretPlaintext, secret).toString() : null, - (accessTokenPlaintext != null) + (StringUtils.isNoneBlank(accessTokenPlaintext)) ? encrypt(accessTokenPlaintext, secret).toString() : null); } 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 6d83370a..acadc63e 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.user.UserActivityLog; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.util.Utils; @@ -234,6 +235,20 @@ public class FilterMap extends POSTMapper { false); } + public Long getUserLogFrom(final String filterAttrFrom) { + return getFromToValue( + UserActivityLog.FILTER_ATTR_FROM_TO, + UserActivityLog.FILTER_ATTR_FROM, + true); + } + + public Long getUserLofTo(final String filterAttrTo) { + return getFromToValue( + UserActivityLog.FILTER_ATTR_FROM_TO, + UserActivityLog.FILTER_ATTR_TO, + false); + } + public String getClientEventText() { return getSQLWildcard(ClientEvent.FILTER_ATTR_TEXT); } 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 47ae1a43..0d7901cd 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 @@ -35,6 +35,12 @@ public interface UserActivityLogDAO extends * @return Result of the Entity or referring to an Error id happened */ public Result logImport(E entity); + /** Create a user activity log entry for the current user of activity type EXPORT + * + * @param entity the Entity + * @return Result of the Entity or referring to an Error id happened */ + public Result logExport(E entity); + /** Create a user activity log entry for the current user of activity type MODIFY * * @param entity the Entity 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 febdbb94..60754457 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 @@ -13,7 +13,9 @@ import static org.mybatis.dynamic.sql.SqlBuilder.isIn; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -38,10 +40,11 @@ import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserActivityLogRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserActivityLogRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.UserActivityLogRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.UserRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; @@ -54,13 +57,16 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { private static final Logger log = LoggerFactory.getLogger(UserActivityLogDAOImpl.class); private final UserActivityLogRecordMapper userLogRecordMapper; + private final UserRecordMapper userRecordMapper; private final UserService userService; public UserActivityLogDAOImpl( final UserActivityLogRecordMapper userLogRecordMapper, + final UserRecordMapper userRecordMapper, final UserService userService) { this.userLogRecordMapper = userLogRecordMapper; + this.userRecordMapper = userRecordMapper; this.userService = userService; } @@ -81,6 +87,12 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { return log(UserLogActivityType.IMPORT, entity); } + @Override + @Transactional + public Result logExport(final E entity) { + return log(UserLogActivityType.EXPORT, entity); + } + @Override @Transactional public Result logModify(final E entity) { @@ -176,7 +188,12 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { final String message) { return Result.tryCatch(() -> { - log(user, activityType, entity.entityType(), entity.getModelId(), message); + String _message = message; + if (message == null) { + _message = "Entity details: " + String.valueOf(entity); + } + + log(user, activityType, entity.entityType(), entity.getModelId(), _message); return entity; }) .onError(TransactionHandler::rollback) @@ -210,7 +227,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { @Transactional(readOnly = true) public Result byPK(final Long id) { return Result.tryCatch(() -> this.userLogRecordMapper.selectByPrimaryKey(id)) - .flatMap(UserActivityLogDAOImpl::toDomainModel); + .flatMap(this::toDomainModel); } @Override @@ -235,28 +252,29 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { @Transactional(readOnly = true) public Result> getAllForUser(final String userUuid) { return Result.tryCatch(() -> { - return this.userLogRecordMapper.selectByExample() - .where( - UserActivityLogRecordDynamicSqlSupport.userUuid, - SqlBuilder.isEqualTo(userUuid)) - .build() - .execute() - .stream() - .map(UserActivityLogDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList()); + + return this.toDomainModel( + this.userService.getCurrentUser().institutionId(), + this.userLogRecordMapper.selectByExample() + .where( + UserActivityLogRecordDynamicSqlSupport.userUuid, + SqlBuilder.isEqualTo(userUuid)) + .build() + .execute()); }); } @Override @Transactional(readOnly = true) - public Result> allMatching(final FilterMap filterMap, + public Result> allMatching( + final FilterMap filterMap, final Predicate predicate) { + return all( filterMap.getInstitutionId(), filterMap.getString(UserActivityLog.FILTER_ATTR_USER), - filterMap.getLong(UserActivityLog.FILTER_ATTR_FROM), - filterMap.getLong(UserActivityLog.FILTER_ATTR_TO), + filterMap.getUserLogFrom(UserActivityLog.FILTER_ATTR_FROM), + filterMap.getUserLofTo(UserActivityLog.FILTER_ATTR_TO), filterMap.getString(UserActivityLog.FILTER_ATTR_ACTIVITY_TYPES), filterMap.getString(UserActivityLog.FILTER_ATTR_ENTITY_TYPES), predicate); @@ -285,37 +303,36 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { ? predicate : model -> true; - return this.userLogRecordMapper.selectByExample() - .join(UserRecordDynamicSqlSupport.userRecord) - .on( - UserRecordDynamicSqlSupport.uuid, - SqlBuilder.equalTo(UserActivityLogRecordDynamicSqlSupport.userUuid)) - .where( - UserRecordDynamicSqlSupport.institutionId, - SqlBuilder.isEqualTo(institutionId)) - .and( - UserActivityLogRecordDynamicSqlSupport.userUuid, - SqlBuilder.isEqualToWhenPresent(userId)) - .and( - UserActivityLogRecordDynamicSqlSupport.timestamp, - SqlBuilder.isGreaterThanOrEqualToWhenPresent(from)) - .and( - UserActivityLogRecordDynamicSqlSupport.timestamp, - SqlBuilder.isLessThanWhenPresent(to)) - .and( - UserActivityLogRecordDynamicSqlSupport.activityType, - SqlBuilder.isInCaseInsensitiveWhenPresent(_activityTypes)) - .and( - UserActivityLogRecordDynamicSqlSupport.entityType, - SqlBuilder.isInCaseInsensitiveWhenPresent(_entityTypes)) - .build() - .execute() + return this.toDomainModel( + institutionId, + this.userLogRecordMapper.selectByExample() + .join(UserRecordDynamicSqlSupport.userRecord) + .on( + UserRecordDynamicSqlSupport.uuid, + SqlBuilder.equalTo(UserActivityLogRecordDynamicSqlSupport.userUuid)) + .where( + UserRecordDynamicSqlSupport.institutionId, + SqlBuilder.isEqualTo(institutionId)) + .and( + UserActivityLogRecordDynamicSqlSupport.userUuid, + SqlBuilder.isEqualToWhenPresent(userId)) + .and( + UserActivityLogRecordDynamicSqlSupport.timestamp, + SqlBuilder.isGreaterThanOrEqualToWhenPresent(from)) + .and( + UserActivityLogRecordDynamicSqlSupport.timestamp, + SqlBuilder.isLessThanWhenPresent(to)) + .and( + UserActivityLogRecordDynamicSqlSupport.activityType, + SqlBuilder.isInCaseInsensitiveWhenPresent(_activityTypes)) + .and( + UserActivityLogRecordDynamicSqlSupport.entityType, + SqlBuilder.isInCaseInsensitiveWhenPresent(_entityTypes)) + .build() + .execute()) .stream() - .map(UserActivityLogDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) .filter(_predicate) .collect(Collectors.toList()); - }); } @@ -323,14 +340,13 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { @Transactional(readOnly = true) public Result> allOf(final Set pks) { return Result.tryCatch(() -> { - return this.userLogRecordMapper.selectByExample() - .where(UserActivityLogRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) - .build() - .execute() - .stream() - .map(UserActivityLogDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList()); + + return this.toDomainModel( + this.userService.getCurrentUser().institutionId(), + this.userLogRecordMapper.selectByExample() + .where(UserActivityLogRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) + .build() + .execute()); }); } @@ -395,11 +411,67 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { this.userLogRecordMapper.updateByPrimaryKeySelective(selective); } - private static Result toDomainModel(final UserActivityLogRecord record) { + private Collection toDomainModel( + final Long institutionId, + final List records) { + + if (records.isEmpty()) { + return Collections.emptyList(); + } + + final Set useruuids = records + .stream() + .map(UserActivityLogRecord::getUserUuid) + .collect(Collectors.toSet()); + + final Map userMapping = this.userRecordMapper + .selectByExample() + .where( + UserRecordDynamicSqlSupport.institutionId, + SqlBuilder.isEqualToWhenPresent(institutionId)) + .and( + UserRecordDynamicSqlSupport.uuid, + SqlBuilder.isIn(new ArrayList<>(useruuids))) + .build() + .execute() + .stream() + .collect(Collectors.toMap(ur -> ur.getUuid(), ur -> ur.getUsername())); + + return records + .stream() + .map(record -> new UserActivityLog( + record.getId(), + record.getUserUuid(), + userMapping.get(record.getUserUuid()), + record.getTimestamp(), + UserLogActivityType.valueOf(record.getActivityType()), + EntityType.valueOf(record.getEntityType()), + record.getEntityId(), + record.getMessage())) + .collect(Collectors.toList()); + } + + private Result toDomainModel(final UserActivityLogRecord record) { return Result.tryCatch(() -> { + + final List user = this.userRecordMapper.selectByExample() + .where( + UserRecordDynamicSqlSupport.uuid, + SqlBuilder.isEqualTo(record.getUserUuid())) + .build() + .execute(); + + String username = record.getUserUuid(); + if (CollectionUtils.isEmpty(user) || user.size() > 1) { + log.error("To many user found for user uuid: {}", record.getUserUuid()); + } else { + username = user.get(0).getUsername(); + } + return new UserActivityLog( record.getId(), record.getUserUuid(), + username, record.getTimestamp(), UserLogActivityType.valueOf(record.getActivityType()), EntityType.valueOf(record.getEntityType()), diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java index c19e289e..5ff1805a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java @@ -33,9 +33,6 @@ final class MockupLmsAPITemplate implements LmsAPITemplate { private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class); - public static final String MOCKUP_LMS_CLIENT_NAME = "mockupLmsClientName"; - public static final String MOCKUP_LMS_CLIENT_SECRET = "mockupLmsClientSecret"; - private final ClientCredentialService clientCredentialService; private final LmsSetup lmsSetup; private final ClientCredentials credentials; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java index 4421a430..b3bfe932 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java @@ -19,7 +19,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.io.IOUtils; +import org.apache.tomcat.util.http.fileupload.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; @@ -78,31 +78,31 @@ public class ExamConfigIO { final ConfigurationFormat exportFormat, final OutputStream out, final Long institutionId, - final Long configurationNodeId) { + final Long configurationNodeId) throws Exception { if (log.isDebugEnabled()) { log.debug("Start export SEB plain XML configuration asynconously"); } - // get all defined root configuration attributes prepared and sorted - final List sortedAttributes = this.configurationAttributeDAO.getAllRootAttributes() - .getOrThrow() - .stream() - .flatMap(this::convertAttribute) - .filter(exportFormatBasedAttributeFilter(exportFormat)) - .sorted() - .collect(Collectors.toList()); - - // get follow-up configurationId for given configurationNodeId - final Long configurationId = this.configurationDAO - .getFollowupConfiguration(configurationNodeId) - .getOrThrow().id; - - final Function configurationValueSupplier = - getConfigurationValueSupplier(institutionId, configurationId); - try { + // get all defined root configuration attributes prepared and sorted + final List sortedAttributes = this.configurationAttributeDAO.getAllRootAttributes() + .getOrThrow() + .stream() + .flatMap(this::convertAttribute) + .filter(exportFormatBasedAttributeFilter(exportFormat)) + .sorted() + .collect(Collectors.toList()); + + // get follow-up configurationId for given configurationNodeId + final Long configurationId = this.configurationDAO + .getFollowupConfiguration(configurationNodeId) + .getOrThrow().id; + + final Function configurationValueSupplier = + getConfigurationValueSupplier(institutionId, configurationId); + writeHeader(exportFormat, out); // write attributes @@ -135,7 +135,6 @@ public class ExamConfigIO { } writeFooter(exportFormat, out); - out.flush(); if (log.isDebugEnabled()) { log.debug("Finished export SEB plain XML configuration asynconously"); @@ -143,12 +142,13 @@ public class ExamConfigIO { } catch (final Exception e) { log.error("Unexpected error while trying to write SEB Exam Configuration XML to output stream: ", e); + throw e; + } finally { try { out.flush(); } catch (final IOException e1) { log.error("Unable to flush output stream after error"); } - } finally { IOUtils.closeQuietly(out); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/PasswordEncryptor.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/PasswordEncryptor.java index d2d3ab05..7c1d36cd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/PasswordEncryptor.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/PasswordEncryptor.java @@ -66,7 +66,6 @@ public class PasswordEncryptor implements SebConfigCryptor { input.close(); encryptOutput.flush(); - encryptOutput.close(); } catch (final CryptorException e) { log.error("Error while trying to stream and encrypt data: ", e); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebClientConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebClientConfigServiceImpl.java index b63ab60c..3d29252e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebClientConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebClientConfigServiceImpl.java @@ -183,6 +183,10 @@ public class SebClientConfigServiceImpl implements SebClientConfigService { EncryptionContext.contextOfPlainText()); } + if (log.isDebugEnabled()) { + log.debug("*** Finished Seb client configuration download streaming composition"); + } + } catch (final Exception e) { log.error("Error while zip and encrypt seb client config stream: ", e); try { @@ -218,7 +222,8 @@ public class SebClientConfigServiceImpl implements SebClientConfigService { return plainTextConfig; } - private String extractXML(final SebClientConfig config, + private String extractXML( + final SebClientConfig config, final CharSequence plainClientId, final CharSequence plainClientSecret) { @@ -294,10 +299,6 @@ public class SebClientConfigServiceImpl implements SebClientConfigService { EncryptionContext.contextOf( Strategy.PASSWORD_PSWD, encryptionPasswordPlaintext)); - - if (log.isDebugEnabled()) { - log.debug("*** Finished Seb client configuration with password based encryption"); - } } /** Get a encoded clientSecret for the SebClientConfiguration with specified clientId/clientName. diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImpl.java index 2002eca5..4157011f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImpl.java @@ -73,6 +73,7 @@ public final class SebConfigEncryptionServiceImpl implements SebConfigEncryption } pout.write(strategy.header); + getEncryptor(strategy) .getOrThrow() .encrypt(pout, input, context); @@ -92,12 +93,6 @@ public final class SebConfigEncryptionServiceImpl implements SebConfigEncryption } catch (final IOException e1) { log.error("Failed to close PipedInputStream: ", e1); } - try { - if (pout != null) - pout.close(); - } catch (final IOException e1) { - log.error("Failed to close PipedOutputStream: ", e1); - } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java index d3ce36fb..a9749f8d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java @@ -203,7 +203,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { return Result.of(configKey); - } catch (final IOException e) { + } catch (final Exception e) { log.error("Error while stream plain JSON SEB clonfiguration data for Config-Key generation: ", e); return Result.ofError(e); } finally { @@ -254,7 +254,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { pout.close(); pin.close(); - } catch (final IOException e) { + } catch (final Exception e) { log.error("Error while stream plain text SEB clonfiguration data: ", e); } finally { try { 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 c0ec1f44..cfa64686 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 @@ -131,7 +131,8 @@ public class ConfigurationNodeController extends EntityController this.sebExamConfigService .exportPlainXML(out, institutionId, modelId); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java index ad7a18ff..93c05014 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java @@ -26,10 +26,12 @@ import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; +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.sebconfig.SebClientConfig; import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange; +import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType; 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.SebClientConfigRecordDynamicSqlSupport; @@ -76,10 +78,20 @@ public class SebClientConfigController extends ActivatableEntityController this.sebClientConfigService - .exportSebClientConfiguration(out, modelId); + this.userActivityLogDAO.log( + UserLogActivityType.EXPORT, + EntityType.SEB_CLIENT_CONFIGURATION, + modelId, + "Export of SEB Client Configuration"); + + final StreamingResponseBody stream = out -> { + this.sebClientConfigService.exportSebClientConfiguration( + out, + modelId); + }; return new ResponseEntity<>(stream, HttpStatus.OK); } 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 8464b861..0336cde9 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,8 +8,8 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; -import java.util.Collection; - +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.springframework.web.bind.annotation.RequestMapping; @@ -23,12 +23,11 @@ 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.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.gbl.util.Utils; 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.dao.UserActivityLogDAO; @WebServiceProfile @@ -57,53 +56,50 @@ public class UserActivityLogController { .addUsersInstitutionDefaultPropertySupport(binder); } - @RequestMapping(method = RequestMethod.GET) + /** 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 = UserActivityLog.FILTER_ATTR_INSTITUTION, + name = API.PARAM_INSTITUTION_ID, required = true, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @RequestParam(name = UserActivityLog.FILTER_ATTR_USER, required = false) final String userId, - @RequestParam(name = UserActivityLog.FILTER_ATTR_FROM, required = false) final String from, - @RequestParam(name = UserActivityLog.FILTER_ATTR_TO, required = false) final String to, - @RequestParam(name = UserActivityLog.FILTER_ATTR_ACTIVITY_TYPES, - required = false) final String activityTypes, - @RequestParam(name = UserActivityLog.FILTER_ATTR_ENTITY_TYPES, required = false) final String entityTypes, @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(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); - return this.paginationService.getPage( + + 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(), - () -> _getAll(institutionId, userId, from, to, activityTypes, entityTypes)).getOrThrow(); - } - - private Result> _getAll( - final Long institutionId, - final String userId, - final String from, - final String to, - final String activityTypes, - final String entityTypes) { - - return Result.tryCatch(() -> { - - this.paginationService.setDefaultLimitIfNotSet(); - - return this.userActivityLogDAO.all( - institutionId, - userId, - Utils.toTimestamp(from), - Utils.toTimestamp(to), - activityTypes, - entityTypes, - Utils.truePredicate()) - .getOrThrow(); - }); + () -> this.userActivityLogDAO.allMatching(filterMap)) + .getOrThrow(); } private void checkBaseReadPrivilege(final Long institutionId) { diff --git a/src/main/resources/data-demo.sql b/src/main/resources/data-demo.sql index 8bb8c644..e984fa93 100644 --- a/src/main/resources/data-demo.sql +++ b/src/main/resources/data-demo.sql @@ -31,7 +31,7 @@ INSERT IGNORE INTO view VALUES (11, 'hooked_keys', 12, 11); INSERT IGNORE INTO lms_setup VALUES - (1, 1, 'test', 'MOCKUP', 'http://', 'ccdfa2330533ed6c316a8ffbd64a3197d4a79956ac7ee4c1162f7bdb1a27234fe8793615a51074351e', '8d14b78ecdcbec1d010d414a7208dbe5c411f1fa735c35c7427d840453093a3730d1bc0abe13b9b1a8', null, 1) + (1, 1, 'test', 'MOCKUP', 'http://', 'test-user', '8d14b78ecdcbec1d010d414a7208dbe5c411f1fa735c35c7427d840453093a3730d1bc0abe13b9b1a8', null, 1) ; INSERT IGNORE INTO seb_client_configuration VALUES diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 15ccd15d..947189bd 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -32,7 +32,7 @@ - + diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 27ce5e32..5d462354 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -21,10 +21,37 @@ sebserver.overall.action.category.varia=Varia sebserver.overall.status.active=Active sebserver.overall.status.inactive=Inactive +sebserver.overall.date.from=From +sebserver.overall.date.to=To + sebserver.overall.action.add=Add; sebserver.overall.action.remove=Remove sebserver.overall.action.select=Please Select +sebserver.overall.types.activityType.CREATE=Create New +sebserver.overall.types.activityType.IMPORT=Import +sebserver.overall.types.activityType.EXPORT=Export +sebserver.overall.types.activityType.MODIFY=Modify +sebserver.overall.types.activityType.PASSWORD_CHANGE=Password Change +sebserver.overall.types.activityType.DEACTIVATE=Deactivate +sebserver.overall.types.activityType.ACTIVATE=Activate +sebserver.overall.types.activityType.DELETE=Delete + +sebserver.overall.types.entityType.CONFIGURATION_ATTRIBUTE=Configuration Attribute +sebserver.overall.types.entityType.CONFIGURATION_VALUE=Configuration Value +sebserver.overall.types.entityType.VIEW=Configuration View +sebserver.overall.types.entityType.ORIENTATION=Configuration Orientation +sebserver.overall.types.entityType.CONFIGURATION=Configuration History Point +sebserver.overall.types.entityType.CONFIGURATION_NODE=Exam Configuration +sebserver.overall.types.entityType.EXAM_CONFIGURATION_MAP=Exam Configuration Mapping +sebserver.overall.types.entityType.EXAM=Exam +sebserver.overall.types.entityType.INDICATOR=Indicator +sebserver.overall.types.entityType.THRESHOLD=Threshold +sebserver.overall.types.entityType.INSTITUTION=Institution +sebserver.overall.types.entityType.SEB_CLIENT_CONFIGURATION=Client Configuration +sebserver.overall.types.entityType.LMS_SETUP=LMS Setup +sebserver.overall.types.entityType.USER=User Account + ################################ # Form validation and messages ################################ @@ -288,6 +315,7 @@ sebserver.exam.configuration.list.column.name=Name sebserver.exam.configuration.list.column.description=Description sebserver.exam.configuration.list.column.status=Status sebserver.exam.configuration.list.empty=There is currently no SEB Configuration defined for this Exam. Please add one +sebserver.exam.configuration.list.pleaseSelect=Please Select a SEB Configuration first sebserver.exam.configuration.action.list.new=Add sebserver.exam.configuration.action.list.modify=Edit @@ -877,7 +905,7 @@ sebserver.monitoring.exam.action.detail.view=Back To Overview sebserver.monitoring.exam.action.list.view=Monitoring -sebserver.monitoring.exam.info.pleaseSelect=Please select an exam first +sebserver.monitoring.exam.info.pleaseSelect=Please select an exam from the list sebserver.monitoring.exam.list.empty=There are currently no running exams sebserver.monitoring.exam.list.column.name=Name @@ -893,7 +921,7 @@ sebserver.monitoring.connection.list.column.status=Status sebserver.monitoring.connection.list.column.examname=Exam sebserver.monitoring.connection.list.column.vdiAddress=IP Address (VDI) -sebserver.monitoring.exam.connection.emptySelection=Please select a connection first +sebserver.monitoring.exam.connection.emptySelection=Please select a connection from the list sebserver.monitoring.exam.connection.title=SEB Client Connection sebserver.monitoring.exam.connection.list.actions=Selected Connection sebserver.monitoring.exam.connection.action.view=View Details @@ -913,3 +941,31 @@ 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 +################################ +# Logs +################################ + +sebserver.logs.activity.main=Logs +sebserver.logs.activity.userlogs=User Logs +sebserver.logs.activity.userlogs.details=Show Details +sebserver.logs.activity.seblogs=SEB Client Logs +sebserver.logs.activity.seblogs.details=Show Details + +sebserver.userlogs.list.title=User Activity Logs +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.message=Message + +sebserver.userlogs.details.title=User Activity Log Details +sebserver.userlogs.info.pleaseSelect=Please select a log from the list +sebserver.userlogs.list.actions=Selected Log +sebserver.userlogs.list.empty=No User activity logs has been found. Please adapt or clear the filter + + +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 + diff --git a/src/main/resources/schema-demo.sql b/src/main/resources/schema-demo.sql index cf0bbef7..dcc10d47 100644 --- a/src/main/resources/schema-demo.sql +++ b/src/main/resources/schema-demo.sql @@ -439,7 +439,7 @@ CREATE TABLE IF NOT EXISTS `user_activity_log` ( `activity_type` VARCHAR(45) NOT NULL, `entity_type` VARCHAR(45) NOT NULL, `entity_id` VARCHAR(255) NOT NULL, - `message` VARCHAR(255) NULL, + `message` VARCHAR(4000) NULL, PRIMARY KEY (`id`)) ; diff --git a/src/main/resources/schema-dev.sql b/src/main/resources/schema-dev.sql index 00e6cb52..dc0f48ca 100644 --- a/src/main/resources/schema-dev.sql +++ b/src/main/resources/schema-dev.sql @@ -461,7 +461,7 @@ CREATE TABLE IF NOT EXISTS `user_activity_log` ( `activity_type` VARCHAR(45) NOT NULL, `entity_type` VARCHAR(45) NOT NULL, `entity_id` VARCHAR(255) NOT NULL, - `message` VARCHAR(255) NULL, + `message` VARCHAR(4000) NULL, PRIMARY KEY (`id`)) ; diff --git a/src/main/resources/static/css/sebserver.css b/src/main/resources/static/css/sebserver.css index 87b89ad8..85fa33eb 100644 --- a/src/main/resources/static/css/sebserver.css +++ b/src/main/resources/static/css/sebserver.css @@ -166,8 +166,8 @@ Composite.login { } Composite.error { - background-color: #aa0000; - margin: 0 0 0 0; + border: 1px solid #aa0000; + border-radius: 1px; } *.header { @@ -230,11 +230,6 @@ Text.error { border: 1px solid #aa0000; } -Text[MULTI] { - padding: 5px 10px 5px 10px; - height: 100px; -} - Text[BORDER], Text[MULTI][BORDER] { border: 1px solid #aaaaaa; border-radius: 0; @@ -252,10 +247,13 @@ Text[BORDER]:focused, Text[MULTI][BORDER]:focused { box-shadow: none; } -Text[BORDER]:disabled, Text[MULTI][BORDER]:disabled, Text[BORDER]:read-only, - Text[MULTI][BORDER]:read-only { +Text:disabled, Text:read-only, Text[BORDER]:disabled, Text[MULTI]:disabled, Text[MULTI][BORDER]:disabled, Text[BORDER]:read-only, Text[MULTI]:read-only, Text[MULTI][BORDER]:read-only { box-shadow: none; - background-color: #f0f0f0; + background-color: #ffffff; + border: none; + border-radius: 0; + color: #4a4a4a; + padding: 0px 0px 0px 0px; } @@ -374,6 +372,12 @@ Shell-Titlebar.message { text-shadow: none; } +Shell-CloseButton:hover.message { + background-color: #82BE1E; + background-gradient-color: #82BE1E; + background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) ); +} + Button { font: 12px Arial, Helvetica, sans-serif; padding: 5px 6px 5px 6px; @@ -488,7 +492,7 @@ Tree[BORDER] { border: 1px solid #eceeef; } -TreeItem { +TreeItem, TreeItem.treesection, Tree-RowOverlay:hover.treesection, Tree-RowOverlay:selected.treesection, Tree-RowOverlay:selected:hover.treesection { font: bold 14px Arial, Helvetica, sans-serif; color: #1f407a; background-color: transparent; @@ -515,6 +519,7 @@ Tree-RowOverlay:hover { color: #1F407A; } + Tree-RowOverlay:selected { background-color: #82be1e; color: #1F407A; @@ -563,30 +568,7 @@ Tree-RowOverlay:selected.actions { color: #4a4a4a; } -Tree-Indent { - width: 16px; - background-image: none; -} -Tree-Indent:collapsed { - background-image: none; -} - -Tree-Indent:collapsed:hover { - background-image: none; -} - -Tree-Indent:expanded { - background-image: none; -} - -Tree-Indent:expanded:hover { - background-image: none; -} - -Tree-Indent:line { - background-image: none; -} /* TabFolder default theme */ diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLogTest.java b/src/test/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLogTest.java index 8a749361..4dea825e 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLogTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLogTest.java @@ -26,6 +26,7 @@ public class UserActivityLogTest { final UserActivityLog testModel = new UserActivityLog( 1L, "testUser", + "testUser", 123l, UserLogActivityType.CREATE, EntityType.EXAM, @@ -35,12 +36,7 @@ public class UserActivityLogTest { final String jsonValue = this.jsonMapper.writeValueAsString(testModel); assertEquals( - "{\"userUuid\":\"testUser\"," - + "\"timestamp\":123," - + "\"activityType\":\"CREATE\"," - + "\"entityType\":\"EXAM\"," - + "\"entityId\":\"321\"," - + "\"message\":\"noComment\"}", + "{\"id\":1,\"userUuid\":\"testUser\",\"username\":\"testUser\",\"timestamp\":123,\"activityType\":\"CREATE\",\"entityType\":\"EXAM\",\"entityId\":\"321\",\"message\":\"noComment\"}", jsonValue); } diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/UserAPITest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/UserAPITest.java index 9911f2ab..a4b86a9f 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/UserAPITest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/UserAPITest.java @@ -24,6 +24,7 @@ import java.util.stream.Stream; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.junit.Test; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -466,7 +467,8 @@ public class UserAPITest extends AdministrationAPIIntegrationTester { this.mockMvc.perform( get(this.endpoint + API.USER_ACCOUNT_ENDPOINT + "/" + createdUser.uuid) .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference() { @@ -482,7 +484,9 @@ public class UserAPITest extends AdministrationAPIIntegrationTester { .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?user=user1&activity_types=CREATE") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -1003,7 +1007,8 @@ public class UserAPITest extends AdministrationAPIIntegrationTester { this.mockMvc .perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?user=user1&from=" + timeNow) - .header("Authorization", "Bearer " + sebAdminToken)) + .header("Authorization", "Bearer " + sebAdminToken) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { 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 7c9e94b8..3df3849b 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 @@ -14,6 +14,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import org.joda.time.DateTime; import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.test.context.jdbc.Sql; import com.fasterxml.jackson.core.type.TypeReference; @@ -31,7 +33,8 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { final String token = getSebAdminAccess(); final Page logs = this.jsonMapper.readValue( this.mockMvc.perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -48,7 +51,8 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { Page logs = this.jsonMapper.readValue( this.mockMvc .perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?user=user4&institutionId=2") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -60,7 +64,8 @@ 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") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -83,7 +88,8 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { Page logs = this.jsonMapper.readValue( this.mockMvc.perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?institutionId=2&from=" + sec2) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -96,7 +102,8 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { this.mockMvc .perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?institutionId=2&from=" + sec2 + "&to=" + sec4) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -110,7 +117,9 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?institutionId=2&from=" + sec2 + "&to=" + sec5) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -124,7 +133,9 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?institutionId=2&from=" + sec2 + "&to=" + sec6) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -139,7 +150,8 @@ 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 + "?activity_types=CREATE") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -153,7 +165,9 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?activity_types=CREATE,MODIFY") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -168,7 +182,9 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?institutionId=2&activity_types=CREATE,MODIFY") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -184,7 +200,8 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { Page logs = this.jsonMapper.readValue( this.mockMvc .perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?entity_types=INSTITUTION") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -198,7 +215,9 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?entity_types=INSTITUTION,EXAM") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -212,7 +231,9 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { .perform( get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?entity_types=INSTITUTION,EXAM&institutionId=2") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -227,7 +248,8 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { final String token = getAdminInstitution1Access(); final Page logs = this.jsonMapper.readValue( this.mockMvc.perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -243,18 +265,21 @@ public class UserActivityLogAPITest extends AdministrationAPIIntegrationTester { // no privilege at all this.mockMvc.perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isForbidden()); // no privilege at all this.mockMvc.perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?user=user4") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isForbidden()); // no privilege to query logs of users of other institution token = getAdminInstitution1Access(); final Page logs = this.jsonMapper.readValue( this.mockMvc.perform(get(this.endpoint + API.USER_ACTIVITY_LOG_ENDPOINT + "?user=user4") - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + token) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { diff --git a/src/test/resources/schema-test.sql b/src/test/resources/schema-test.sql index c5c62098..0d960732 100644 --- a/src/test/resources/schema-test.sql +++ b/src/test/resources/schema-test.sql @@ -445,7 +445,7 @@ CREATE TABLE IF NOT EXISTS `user_activity_log` ( `activity_type` VARCHAR(45) NOT NULL, `entity_type` VARCHAR(45) NOT NULL, `entity_id` VARCHAR(255) NOT NULL, - `message` VARCHAR(255) NULL, + `message` VARCHAR(4000) NULL, PRIMARY KEY (`id`)) ;