SEBSERV-133 added deletion to SEB client events

This commit is contained in:
anhefti 2020-07-28 15:34:54 +02:00
parent 532ca816bc
commit 9438206c9d
24 changed files with 601 additions and 26 deletions

View file

@ -8,7 +8,9 @@
package ch.ethz.seb.sebserver.gbl.model;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonCreator;
@ -113,4 +115,11 @@ public class EntityProcessingReport {
return builder.toString();
}
public static EntityProcessingReport ofEmptyError() {
return new EntityProcessingReport(
Collections.emptyList(),
Collections.emptyList(),
Arrays.asList(new ErrorEntry(null, APIMessage.ErrorMessage.RESOURCE_NOT_FOUND.of())));
}
}

View file

@ -0,0 +1,152 @@
/*
* Copyright (c) 2020 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 java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
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.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.event.ActionEvent;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModelInputWizard;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModelInputWizard.WizardAction;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModelInputWizard.WizardPage;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.DeleteAllClientEvents;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy
@Component
@GuiProfile
public class SEBClientEventDeletePopup {
private static final Logger log = LoggerFactory.getLogger(SEBClientEventDeletePopup.class);
private final static LocTextKey FORM_TITLE =
new LocTextKey("sebserver.seblogs.delete.form.title");
private final static LocTextKey ACTION_DELETE =
new LocTextKey("sebserver.seblogs.delete.action.delete");
private final static LocTextKey DELETE_CONFIRM_TITLE =
new LocTextKey("sebserver.seblogs.delete.confirm.title");
private final PageService pageService;
protected SEBClientEventDeletePopup(final PageService pageService) {
this.pageService = pageService;
}
public Function<PageAction, PageAction> deleteWizardFunction(final PageContext pageContext) {
return action -> {
final ModelInputWizard<PageContext> wizard =
new ModelInputWizard<PageContext>(
action.pageContext().getParent().getShell(),
this.pageService.getWidgetFactory())
.setVeryLargeDialogWidth();
final String page1Id = "DELETE_PAGE";
final Predicate<PageContext> callback = pc -> doDelete(this.pageService, pc);
final BiFunction<PageContext, Composite, Supplier<PageContext>> composePage1 =
(prefPageContext, content) -> composeDeleteDialog(content,
(prefPageContext != null) ? prefPageContext : pageContext);
final WizardPage<PageContext> page1 = new WizardPage<>(
page1Id,
true,
composePage1,
new WizardAction<>(ACTION_DELETE, callback));
wizard.open(FORM_TITLE, Utils.EMPTY_EXECUTION, page1);
return action;
};
}
private boolean doDelete(
final PageService pageService,
final PageContext pageContext) {
try {
final String idsToDelete = pageContext.getAttribute(PageContext.AttributeKeys.ENTITY_ID_LIST);
final RestCall<EntityProcessingReport>.RestCallBuilder restCallBuilder = this.pageService.getRestService()
.getBuilder(DeleteAllClientEvents.class)
.withFormParam(API.PARAM_MODEL_ID_LIST, idsToDelete)
.withFormParam(API.PARAM_BULK_ACTION_TYPE, BulkActionType.HARD_DELETE.name());
final EntityProcessingReport report = restCallBuilder.call().getOrThrow();
final PageAction action = this.pageService.pageActionBuilder(pageContext)
.newAction(ActionDefinition.LOGS_SEB_CLIENT)
.create();
this.pageService.firePageEvent(
new ActionEvent(action),
action.pageContext());
pageContext.publishPageMessage(
DELETE_CONFIRM_TITLE,
new LocTextKey(
"sebserver.seblogs.delete.confirm.message",
report.results.size(),
(report.errors.isEmpty()) ? "no" : String.valueOf((report.errors.size()))));
return true;
} catch (final Exception e) {
log.error("Unexpected error while trying to delete SEB client logs:", e);
pageContext.notifyUnexpectedError(e);
return false;
}
}
private Supplier<PageContext> composeDeleteDialog(
final Composite parent,
final PageContext pageContext) {
final String idsToDelete = pageContext.getAttribute(PageContext.AttributeKeys.ENTITY_ID_LIST);
final int number = (StringUtils.isNotBlank(idsToDelete))
? idsToDelete.split(Constants.LIST_SEPARATOR).length
: 0;
final Composite grid = this.pageService.getWidgetFactory()
.createPopupScrollComposite(parent);
final Label title = this.pageService.getWidgetFactory().labelLocalized(
grid,
CustomVariant.TEXT_H3,
new LocTextKey("sebserver.seblogs.delete.form.info", number));
final GridData gridData = new GridData();
gridData.horizontalIndent = 10;
gridData.verticalIndent = 10;
title.setLayoutData(gridData);
return () -> pageContext;
}
}

View file

@ -8,16 +8,25 @@
package ch.ethz.seb.sebserver.gui.content;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.tomcat.util.buf.StringUtils;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityName;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
@ -31,8 +40,10 @@ 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.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientEventNames;
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;
@ -42,7 +53,9 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Lazy
@Component
@GuiProfile
public class SEBClientLogs implements TemplateComposer {
public class SEBClientEvents implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(SEBClientEvents.class);
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.title");
@ -71,11 +84,13 @@ public class SEBClientLogs implements TemplateComposer {
private final RestService restService;
private final I18nSupport i18nSupport;
private final SEBClientLogDetailsPopup sebClientLogDetailsPopup;
private final SEBClientEventDeletePopup sebClientEventDeletePopup;
private final int pageSize;
public SEBClientLogs(
public SEBClientEvents(
final PageService pageService,
final SEBClientLogDetailsPopup sebClientLogDetailsPopup,
final SEBClientEventDeletePopup sebClientEventDeletePopup,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
@ -83,6 +98,7 @@ public class SEBClientLogs implements TemplateComposer {
this.restService = this.resourceService.getRestService();
this.i18nSupport = this.resourceService.getI18nSupport();
this.sebClientLogDetailsPopup = sebClientLogDetailsPopup;
this.sebClientEventDeletePopup = sebClientEventDeletePopup;
this.pageSize = pageSize;
this.examFilter = new TableFilterAttribute(
@ -113,6 +129,9 @@ public class SEBClientLogs implements TemplateComposer {
.clearEntityKeys()
.clearAttributes());
final boolean writeGrant = this.pageService.getCurrentUser()
.hasInstitutionalPrivilege(PrivilegeType.WRITE, EntityType.CLIENT_EVENT);
// table
final EntityTable<ExtendedClientEvent> table = this.pageService.entityTableBuilder(
this.restService.getRestCall(GetExtendedClientEventPage.class))
@ -185,7 +204,37 @@ public class SEBClientLogs implements TemplateComposer {
action -> this.sebClientLogDetailsPopup.showDetails(action, table.getSingleSelectedROWData()),
EMPTY_SELECTION_TEXT)
.noEventPropagation()
.publishIf(table::hasAnyContent, false);
.publishIf(table::hasAnyContent, false)
.newAction(ActionDefinition.LOGS_SEB_CLIENT_DELETE_ALL)
.withExec(action -> this.getOpenDelete(action, table.getFilterCriteria()))
.noEventPropagation()
.publishIf(() -> writeGrant);
}
private PageAction getOpenDelete(final PageAction pageAction, final MultiValueMap<String, String> filterCriteria) {
try {
final List<String> ids = this.restService.getBuilder(GetClientEventNames.class)
.withQueryParams(filterCriteria)
.call()
.getOrThrow()
.stream()
.map(EntityName::getModelId)
.collect(Collectors.toList());
final PageAction deleteAction = pageAction.withAttribute(
PageContext.AttributeKeys.ENTITY_ID_LIST,
StringUtils.join(ids, Constants.COMMA))
.withAttribute(
PageContext.AttributeKeys.ENTITY_LIST_TYPE,
EntityType.CLIENT_EVENT.name());
return this.sebClientEventDeletePopup.deleteWizardFunction(deleteAction.pageContext())
.apply(deleteAction);
} catch (final Exception e) {
log.error("Unexpected error while try to open SEB client log delete popup", e);
return pageAction;
}
}
private Function<ExtendedClientEvent, String> examNameFunction() {

View file

@ -683,6 +683,11 @@ public enum ActionDefinition {
new LocTextKey("sebserver.logs.activity.seblogs.details"),
ImageIcon.SHOW,
ActionCategory.LOGS_SEB_CLIENT_LIST),
LOGS_SEB_CLIENT_DELETE_ALL(
new LocTextKey("sebserver.seblogs.action.delete"),
ImageIcon.DELETE,
PageStateDefinitionImpl.SEB_CLIENT_LOGS,
ActionCategory.FORM),
;

View file

@ -24,7 +24,7 @@ import ch.ethz.seb.sebserver.gui.content.MonitoringRunningExamList;
import ch.ethz.seb.sebserver.gui.content.QuizLookupList;
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.SEBClientEvents;
import ch.ethz.seb.sebserver.gui.content.SEBExamConfigList;
import ch.ethz.seb.sebserver.gui.content.SEBExamConfigForm;
import ch.ethz.seb.sebserver.gui.content.SEBSettingsForm;
@ -85,7 +85,7 @@ public enum PageStateDefinitionImpl implements PageStateDefinition {
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)
SEB_CLIENT_LOGS(Type.LIST_VIEW, SEBClientEvents.class, ActivityDefinition.SEB_CLIENT_LOGS)
;

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.gui.service.page;
import java.util.List;
import java.util.function.Consumer;
import org.eclipse.swt.widgets.Composite;
@ -39,6 +40,8 @@ public interface PageContext {
String PARENT_ENTITY_ID = "PARENT_ENTITY_ID";
String ENTITY_TYPE = "ENTITY_TYPE";
String PARENT_ENTITY_TYPE = "PARENT_ENTITY_TYPE";
String ENTITY_ID_LIST = "ENTITY_ID_LIST";
String ENTITY_LIST_TYPE = "ENTITY_TYPE";
String IMPORT_FROM_QUIZ_DATA = "IMPORT_FROM_QUIZ_DATA";
@ -149,6 +152,11 @@ public interface PageContext {
* @return the EntityKey of the parent Entity that is associated within this PageContext */
EntityKey getParentEntityKey();
/** Get a list of entity keys within the attribute ENTITY_ID_LIST and ENTITY_LIST_TYPE.
*
* @return A list of entity keys if available from the attributes map */
List<EntityKey> getEntityKeyList();
/** Adds a given EntityKey as base Entity key to a new PageContext that is returned as a copy of this PageContext.
*
* @param entityKey the EntityKey to add as base Entity key
@ -161,6 +169,12 @@ public interface PageContext {
* @return the new PageContext with the EntityKey added */
PageContext withParentEntityKey(EntityKey entityKey);
/** Adds a given collection of EntityKey to a new PageContext that is returned as a copy of this PageContext.
*
* @param entityKeys the list of EntityKey to add
* @return the new PageContext with the list of EntityKey added */
PageContext withEntityKeys(List<EntityKey> entityKeys);
/** Create a copy of this PageContext and resets both entity keys attributes, the base and the parent EntityKey
*
* @return copy of this PageContext with reset EntityKey attributes (base and parent) */

View file

@ -565,6 +565,11 @@ public interface PageService {
return this;
}
public PageActionBuilder withEntityKeys(final List<EntityKey> entityKeys) {
this.pageContext = this.pageContext.withEntityKeys(entityKeys);
return this;
}
public PageActionBuilder ignoreMoveAwayFromEdit() {
this.ignoreMoveAwayFromEdit = true;
return this;

View file

@ -8,12 +8,16 @@
package ch.ethz.seb.sebserver.gui.service.page.impl;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.widgets.DialogCallback;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
@ -22,6 +26,7 @@ import org.eclipse.swt.widgets.Shell;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessageError;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
@ -39,6 +44,8 @@ public class PageContextImpl implements PageContext {
private static final Logger log = LoggerFactory.getLogger(PageContextImpl.class);
private static final String ENTITY_LIST_TYPE = null;
private final I18nSupport i18nSupport;
private final ComposerService composerService;
private final Composite root;
@ -185,6 +192,34 @@ public class PageContextImpl implements PageContext {
.withAttribute(AttributeKeys.PARENT_ENTITY_TYPE, entityKey.entityType.name());
}
@Override
public List<EntityKey> getEntityKeyList() {
if (hasAttribute(AttributeKeys.ENTITY_ID_LIST) && hasAttribute(AttributeKeys.ENTITY_LIST_TYPE)) {
final EntityType type = EntityType.valueOf(getAttribute(ENTITY_LIST_TYPE));
Arrays.asList(StringUtils.split(getAttribute(AttributeKeys.ENTITY_ID_LIST), Constants.COMMA))
.stream()
.map(id -> new EntityKey(id, type))
.collect(Collectors.toList());
}
return Collections.emptyList();
}
@Override
public PageContext withEntityKeys(final List<EntityKey> entityKeys) {
if (entityKeys == null || entityKeys.isEmpty()) {
return removeAttribute(AttributeKeys.ENTITY_ID_LIST)
.removeAttribute(AttributeKeys.ENTITY_LIST_TYPE);
}
final List<String> ids = entityKeys
.stream()
.map(EntityKey::getModelId)
.collect(Collectors.toList());
final String joinedIds = StringUtils.join(ids, Constants.COMMA);
return withAttribute(AttributeKeys.ENTITY_ID_LIST, joinedIds)
.withAttribute(AttributeKeys.ENTITY_LIST_TYPE, entityKeys.get(0).entityType.name());
}
@Override
public PageContext clearEntityKeys() {
return withEntityKey(null)

View file

@ -316,6 +316,18 @@ public abstract class RestCall<T> {
return this;
}
public RestCallBuilder withFormParams(final MultiValueMap<String, String> params) {
if (params != null) {
params.entrySet()
.stream()
.forEach(param -> {
final String name = param.getKey();
param.getValue().stream().forEach(p -> withFormParam(name, p));
});
}
return this;
}
public RestCallBuilder withPaging(final int pageNumber, final int pageSize) {
this.queryParams.put(Page.ATTR_PAGE_NUMBER, Arrays.asList(String.valueOf(pageNumber)));
this.queryParams.put(Page.ATTR_PAGE_SIZE, Arrays.asList(String.valueOf(pageSize)));
@ -375,7 +387,6 @@ public abstract class RestCall<T> {
+ this.queryParams
+ ", uriVariables=" + this.uriVariables + "]";
}
}
public static final class TypeKey<T> {

View file

@ -28,7 +28,7 @@ public class DeleteExam extends RestCall<EntityProcessingReport> {
public DeleteExam() {
super(new TypeKey<>(
CallType.ACTIVATION_DEACTIVATE,
CallType.DELETE,
EntityType.EXAM,
new TypeReference<EntityProcessingReport>() {
}),

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class DeleteAllClientEvents extends RestCall<EntityProcessingReport> {
public DeleteAllClientEvents() {
super(new TypeKey<>(
CallType.DELETE,
EntityType.CLIENT_EVENT,
new TypeReference<EntityProcessingReport>() {
}),
HttpMethod.DELETE,
MediaType.APPLICATION_FORM_URLENCODED,
API.SEB_CLIENT_EVENT_ENDPOINT);
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
import java.util.List;
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.EntityName;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetClientEventNames extends RestCall<List<EntityName>> {
public GetClientEventNames() {
super(new TypeKey<>(
CallType.GET_NAMES,
EntityType.CLIENT_EVENT,
new TypeReference<List<EntityName>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.SEB_CLIENT_EVENT_ENDPOINT + API.NAMES_PATH_SEGMENT);
}
}

View file

@ -37,6 +37,7 @@ import org.eclipse.swt.widgets.Widget;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.HtmlUtils;
@ -294,6 +295,14 @@ public class EntityTable<ROW> {
}
}
public MultiValueMap<String, String> getFilterCriteria() {
if (this.filter == null) {
return new LinkedMultiValueMap<>();
}
return this.filter.getFilterParameter();
}
public void applySort(final String columnName) {
try {
this.sortColumn = columnName;

View file

@ -179,7 +179,15 @@ public class AuthorizationServiceImpl implements AuthorizationService {
.andForRole(UserRole.INSTITUTIONAL_ADMIN)
.withInstitutionalPrivilege(PrivilegeType.READ)
.andForRole(UserRole.EXAM_ADMIN)
.withInstitutionalPrivilege(PrivilegeType.READ)
.withInstitutionalPrivilege(PrivilegeType.WRITE)
.andForRole(UserRole.EXAM_SUPPORTER)
.withOwnerPrivilege(PrivilegeType.READ)
.create();
// grants for SEB client events
addPrivilege(EntityType.CLIENT_EVENT)
.forRole(UserRole.EXAM_ADMIN)
.withInstitutionalPrivilege(PrivilegeType.WRITE)
.andForRole(UserRole.EXAM_SUPPORTER)
.withOwnerPrivilege(PrivilegeType.READ)
.create();

View file

@ -8,12 +8,12 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent;
import static org.mybatis.dynamic.sql.SqlBuilder.isIn;
import static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -79,7 +79,17 @@ public class ClientEventDAOImpl implements ClientEventDAO {
return Result.tryCatch(() -> this.clientEventRecordMapper
.selectByExample()
.leftJoin(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord)
.on(
ClientConnectionRecordDynamicSqlSupport.id,
equalTo(ClientEventRecordDynamicSqlSupport.clientConnectionId))
.where(
ClientConnectionRecordDynamicSqlSupport.institutionId,
isEqualToWhenPresent(filterMap.getInstitutionId()))
.and(
ClientConnectionRecordDynamicSqlSupport.examId,
isEqualToWhenPresent(filterMap.getClientEventExamId()))
.and(
ClientEventRecordDynamicSqlSupport.clientConnectionId,
isEqualToWhenPresent(filterMap.getClientEventConnectionId()))
.and(
@ -205,9 +215,22 @@ public class ClientEventDAOImpl implements ClientEventDAO {
@Override
@Transactional
public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) {
throw new UnsupportedOperationException(
"Delete is not supported for particular client events. "
+ "Use delete of a client connection to delete also all client events of this connection.");
return Result.tryCatch(() -> {
return all
.stream()
.map(EntityKey::getModelId)
.map(Long::parseLong)
.map(pk -> {
final int deleted = this.clientEventRecordMapper.deleteByPrimaryKey(pk);
if (deleted == 1) {
return new EntityKey(String.valueOf(pk), EntityType.CLIENT_EVENT);
} else {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
});
}
private Result<ClientEventRecord> recordById(final Long id) {

View file

@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
@ -48,18 +47,15 @@ public class MoodleCourseRestriction {
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY = "browserKey";
private final JSONMapper jsonMapper;
private final LmsSetup lmsSetup;
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private MoodleAPIRestTemplate restTemplate;
protected MoodleCourseRestriction(
final JSONMapper jsonMapper,
final LmsSetup lmsSetup,
final MoodleRestTemplateFactory moodleRestTemplateFactory) {
this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup;
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
}

View file

@ -75,7 +75,6 @@ public class MoodleLmsAPITemplateFactory {
final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction(
this.jsonMapper,
lmsSetup,
moodleRestTemplateFactory);
return new MoodleLmsAPITemplate(

View file

@ -630,7 +630,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
// check Exam has an default SEB Exam configuration attached
if (!this.examSessionService.hasDefaultConfigurationAttached(examId)) {
throw new APIConstraintViolationException(
"Exam is currently has no default SEB Exam configuration attached");
"Exam is currently running but has no default SEB Exam configuration attached");
}
}

View file

@ -8,8 +8,13 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@ -23,14 +28,20 @@ import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.EntityDependency;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport.ErrorEntry;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
@ -49,7 +60,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_EVENT_ENDPOINT)
public class ClientEventController extends ReadonlyEntityController<ClientEvent, ClientEvent> {
private final ExamDAO examDAO;
private final ExamDAO examDao;
private final ClientEventDAO clientEventDAO;
protected ClientEventController(
@ -59,7 +70,7 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ExamDAO examDAO) {
final ExamDAO examDao) {
super(authorization,
bulkActionService,
@ -68,7 +79,7 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
paginationService,
beanValidationService);
this.examDAO = examDAO;
this.examDao = examDao;
this.clientEventDAO = entityDAO;
}
@ -114,6 +125,45 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
}
}
@Override
@RequestMapping(
method = RequestMethod.DELETE,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public EntityProcessingReport hardDeleteAll(
@RequestParam(name = API.PARAM_MODEL_ID_LIST) final List<String> ids,
@RequestParam(name = API.PARAM_BULK_ACTION_ADD_INCLUDES, defaultValue = "false") final boolean addIncludes,
@RequestParam(name = API.PARAM_BULK_ACTION_INCLUDES, required = false) final List<String> includes,
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
this.checkWritePrivilege(institutionId);
if (ids == null || ids.isEmpty()) {
return EntityProcessingReport.ofEmptyError();
}
final Set<EntityKey> sources = ids.stream()
.map(id -> new EntityKey(id, EntityType.CLIENT_EVENT))
.collect(Collectors.toSet());
final Result<Collection<EntityKey>> delete = this.clientEventDAO.delete(sources);
if (delete.hasError()) {
return new EntityProcessingReport(
Collections.emptyList(),
Collections.emptyList(),
Arrays.asList(new ErrorEntry(null, APIMessage.ErrorMessage.UNEXPECTED.of(delete.getError()))));
} else {
return new EntityProcessingReport(
sources,
delete.get(),
Collections.emptyList());
}
}
@Override
public Collection<EntityDependency> getDependencies(
final String modelId,
@ -128,10 +178,34 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
return ClientEventRecordDynamicSqlSupport.clientEventRecord;
}
@Override
protected Result<ClientEvent> checkReadAccess(final ClientEvent entity) {
return Result.tryCatch(() -> {
final EnumSet<UserRole> userRoles = this.authorization
.getUserService()
.getCurrentUser()
.getUserRoles();
final boolean isSupporterOnly = userRoles.size() == 1 && userRoles.contains(UserRole.EXAM_SUPPORTER);
if (isSupporterOnly) {
// check owner grant be getting exam
return super.checkReadAccess(entity)
.getOrThrow();
} else {
// institutional read access
return entity;
}
});
}
@Override
protected boolean hasReadAccess(final ClientEvent entity) {
return !checkReadAccess(entity).hasError();
}
@Override
protected GrantEntity toGrantEntity(final ClientEvent entity) {
return this.examDAO
.byClientConnection(entity.connectionId)
return this.examDao
.byPK(entity.connectionId)
.get();
}

View file

@ -339,6 +339,46 @@ public abstract class EntityController<T extends Entity, M extends Entity> {
.getOrThrow();
}
// **************************
// * DELETE ALL (hard-delete)
// **************************
@RequestMapping(
method = RequestMethod.DELETE,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public EntityProcessingReport hardDeleteAll(
@RequestParam(name = API.PARAM_MODEL_ID_LIST) final List<String> ids,
@RequestParam(name = API.PARAM_BULK_ACTION_ADD_INCLUDES, defaultValue = "false") final boolean addIncludes,
@RequestParam(name = API.PARAM_BULK_ACTION_INCLUDES, required = false) final List<String> includes,
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
this.checkWritePrivilege(institutionId);
if (ids == null || ids.isEmpty()) {
return EntityProcessingReport.ofEmptyError();
}
final EntityType entityType = this.entityDAO.entityType();
final Collection<EntityKey> sources = ids.stream()
.map(id -> new EntityKey(id, entityType))
.collect(Collectors.toList());
final BulkAction bulkAction = new BulkAction(
BulkActionType.HARD_DELETE,
entityType,
sources,
convertToEntityType(addIncludes, includes));
return this.bulkActionService
.createReport(bulkAction)
.flatMap(this::notifyAllDeleted)
.getOrThrow();
}
protected EnumSet<EntityType> convertToEntityType(final boolean addIncludes, final List<String> includes) {
final EnumSet<EntityType> includeDependencies = (includes != null)
? (includes.isEmpty())
@ -387,6 +427,13 @@ public abstract class EntityController<T extends Entity, M extends Entity> {
institutionId);
}
protected void checkWritePrivilege(final Long institutionId) {
this.authorization.check(
PrivilegeType.WRITE,
getGrantEntityType(),
institutionId);
}
protected Result<Collection<T>> getAll(final FilterMap filterMap) {
return this.entityDAO.allMatching(
filterMap,
@ -430,6 +477,10 @@ public abstract class EntityController<T extends Entity, M extends Entity> {
return Result.of(pair);
}
protected Result<EntityProcessingReport> notifyAllDeleted(final EntityProcessingReport pair) {
return Result.of(pair);
}
protected Result<T> checkReadAccess(final T entity) {
final GrantEntity grantEntity = toGrantEntity(entity);
if (grantEntity != null) {

View file

@ -13,6 +13,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
@ -68,6 +69,22 @@ public abstract class ReadonlyEntityController<T extends Entity, M extends Entit
throw new UnsupportedOperationException(ONLY_READ_ACCESS);
}
@Override
public EntityProcessingReport hardDeleteAll(
final List<String> ids,
final boolean addIncludes,
final List<String> includes,
final Long institutionId) {
throw new UnsupportedOperationException(ONLY_READ_ACCESS);
}
@Override
protected SqlTable getSQLTableOfEntity() {
// TODO Auto-generated method stub
return null;
}
@Override
protected M createNew(final POSTMapper postParams) {
throw new UnsupportedOperationException(ONLY_READ_ACCESS);

View file

@ -5,3 +5,5 @@ server.port=8080
server.servlet.context-path=/
server.tomcat.uri-encoding=UTF-8
logging.level.ch=DEBUG

View file

@ -616,7 +616,7 @@ sebserver.exam.delete.report.list.type=Type
sebserver.exam.delete.report.list.name=Name
sebserver.exam.delete.report.list.description=Description
sebserver.exam.delete.action.delete=Delete Exam
sebserver.exam.delete.confirm.title==Deletion Successful
sebserver.exam.delete.confirm.title=Deletion Successful
sebserver.exam.delete.confirm.message=The Exam ({0}) was successfully deleted.<br/>Also the following number dependencies where successfully deleted: {1}.<br/><br/>And there where {2} errors.
sebserver.exam.delete.report.list.empty=No dependencies will be deleted.
@ -1504,4 +1504,11 @@ sebserver.seblogs.form.column.exam.type.tooltip=The type of the exam
sebserver.seblogs.form.column.exam.startTime=Start Time
sebserver.seblogs.form.column.exam.startTime.tooltip=The start date and time of the exam
sebserver.seblogs.form.column.exam.endTime=End Time
sebserver.seblogs.form.column.exam.endTime.tooltip=The end date and time of the exam
sebserver.seblogs.form.column.exam.endTime.tooltip=The end date and time of the exam
sebserver.seblogs.action.delete=Delete Logs
sebserver.seblogs.delete.form.title=Delete SEB Logs
sebserver.seblogs.delete.form.info=This will delete all SEB client logs from the current filtered list.<br/>Please check carefully if all SEB client logs from the list shall be deleted.<br/><br/>There are currently {0} logs within the list.
sebserver.seblogs.delete.action.delete=Delete All Logs
sebserver.seblogs.delete.confirm.title=Deletion Successful
sebserver.seblogs.delete.confirm.message={0} SEB client logs where successfully deleted.<br/><br/>And there where {1} errors.

View file

@ -40,6 +40,7 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCreationInfo;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus;
@ -125,6 +126,10 @@ public class ModelObjectJSONGenerator {
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
domainObject = new Configuration(1L, 1L, 1L, "v1", DateTime.now(), false);
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
domainObject = new View(1L, "name", 20, 1, 1L);
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
@ -265,6 +270,29 @@ public class ModelObjectJSONGenerator {
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
domainObject = new EntityDependency(
new EntityKey(1L, EntityType.EXAM),
new EntityKey(1L, EntityType.INDICATOR),
"IndicatorName", "some description");
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
domainObject = new EntityProcessingReport(
Arrays.asList(new EntityKey(1L, EntityType.EXAM)),
Arrays.asList(new EntityKey(1L, EntityType.INDICATOR), new EntityKey(2L, EntityType.INDICATOR)),
Arrays.asList(new EntityProcessingReport.ErrorEntry(new EntityKey(2L, EntityType.INDICATOR),
APIMessage.ErrorMessage.UNEXPECTED.of())));
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
domainObject = APIMessage.ErrorMessage.UNEXPECTED.of(
new RuntimeException("some unexpected exception"),
"attribute1",
"attribute2",
"attribute3");
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
}
}