finished GUI refactoring

This commit is contained in:
anhefti 2020-02-17 16:43:08 +01:00
parent 000e8c3c7d
commit 26561288c9
31 changed files with 7533 additions and 7178 deletions

View file

@ -1,287 +1,294 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.model.exam;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.util.Utils;
public final class QuizData implements GrantEntity {
public static final String FILTER_ATTR_START_TIME = "start_timestamp";
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "ADDITIONAL_ATTRIBUTES";
public static final String QUIZ_ATTR_ID = "quiz_id";
public static final String QUIZ_ATTR_INSTITUION_ID = Domain.EXAM.ATTR_INSTITUTION_ID;
public static final String QUIZ_ATTR_LMS_SETUP_ID = "lms_setup_id";
public static final String QUIZ_ATTR_LMS_TYPE = "lms_setup_type";
public static final String QUIZ_ATTR_NAME = "quiz_name";
public static final String QUIZ_ATTR_DESCRIPTION = "quiz_description";
public static final String QUIZ_ATTR_START_TIME = "quiz_start_time";
public static final String QUIZ_ATTR_END_TIME = "quiz_end_time";
public static final String QUIZ_ATTR_START_URL = "quiz_start_url";
@JsonProperty(QUIZ_ATTR_ID)
public final String id;
@JsonProperty(QUIZ_ATTR_INSTITUION_ID)
public final Long institutionId;
@JsonProperty(QUIZ_ATTR_LMS_SETUP_ID)
public final Long lmsSetupId;
@JsonProperty(QUIZ_ATTR_LMS_TYPE)
public final LmsType lmsType;
@JsonProperty(QUIZ_ATTR_NAME)
public final String name;
@JsonProperty(QUIZ_ATTR_DESCRIPTION)
public final String description;
@JsonProperty(QUIZ_ATTR_START_TIME)
public final DateTime startTime;
@JsonProperty(QUIZ_ATTR_END_TIME)
public final DateTime endTime;
@JsonProperty(QUIZ_ATTR_START_URL)
public final String startURL;
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES)
public final Map<String, String> additionalAttributes;
@JsonCreator
public QuizData(
@JsonProperty(QUIZ_ATTR_ID) final String id,
@JsonProperty(QUIZ_ATTR_INSTITUION_ID) final Long institutionId,
@JsonProperty(QUIZ_ATTR_LMS_SETUP_ID) final Long lmsSetupId,
@JsonProperty(QUIZ_ATTR_LMS_TYPE) final LmsType lmsType,
@JsonProperty(QUIZ_ATTR_NAME) final String name,
@JsonProperty(QUIZ_ATTR_DESCRIPTION) final String description,
@JsonProperty(QUIZ_ATTR_START_TIME) final DateTime startTime,
@JsonProperty(QUIZ_ATTR_END_TIME) final DateTime endTime,
@JsonProperty(QUIZ_ATTR_START_URL) final String startURL,
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
this.id = id;
this.institutionId = institutionId;
this.lmsSetupId = lmsSetupId;
this.lmsType = lmsType;
this.name = name;
this.description = description;
this.startTime = startTime;
this.endTime = endTime;
this.startURL = startURL;
this.additionalAttributes = Utils.immutableMapOf(additionalAttributes);
}
public QuizData(
final String id,
final Long institutionId,
final Long lmsSetupId,
final LmsType lmsType,
final String name,
final String description,
final String startTime,
final String endTime,
final String startURL) {
this(id, institutionId, lmsSetupId, lmsType, name, description,
startTime, endTime, startURL, Collections.emptyMap());
}
public QuizData(
final String id,
final Long institutionId,
final Long lmsSetupId,
final LmsType lmsType,
final String name,
final String description,
final String startTime,
final String endTime,
final String startURL,
final Map<String, String> additionalAttributes) {
this.id = id;
this.institutionId = institutionId;
this.lmsSetupId = lmsSetupId;
this.lmsType = lmsType;
this.name = name;
this.description = description;
this.startTime = (startTime != null)
? LocalDateTime
.parse(startTime, Constants.STANDARD_DATE_TIME_FORMATTER)
.toDateTime(DateTimeZone.UTC)
: null;
this.endTime = (endTime != null)
? LocalDateTime
.parse(endTime, Constants.STANDARD_DATE_TIME_FORMATTER)
.toDateTime(DateTimeZone.UTC)
: null;
this.startURL = startURL;
this.additionalAttributes = Utils.immutableMapOf(additionalAttributes);
}
@Override
public String getModelId() {
if (this.id == null) {
return null;
}
return String.valueOf(this.id);
}
@Override
public EntityType entityType() {
return EntityType.EXAM;
}
public String geId() {
return this.id;
}
@Override
public Long getInstitutionId() {
return this.institutionId;
}
public Long getLmsSetupId() {
return this.lmsSetupId;
}
public LmsType getLmsType() {
return this.lmsType;
}
@Override
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
public DateTime getStartTime() {
return this.startTime;
}
public DateTime getEndTime() {
return this.endTime;
}
public String getStartURL() {
return this.startURL;
}
public Map<String, String> getAdditionalAttributes() {
return this.additionalAttributes;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("QuizData [id=");
builder.append(this.id);
builder.append(", institutionId=");
builder.append(this.institutionId);
builder.append(", lmsSetupId=");
builder.append(this.lmsSetupId);
builder.append(", lmsType=");
builder.append(this.lmsType);
builder.append(", name=");
builder.append(this.name);
builder.append(", description=");
builder.append(this.description);
builder.append(", startTime=");
builder.append(this.startTime);
builder.append(", endTime=");
builder.append(this.endTime);
builder.append(", startURL=");
builder.append(this.startURL);
builder.append("]");
return builder.toString();
}
public static Comparator<QuizData> getIdComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.id == null)
? 1
: (qd2 == null || qd2.id == null)
? -1
: qd1.id.compareTo(qd2.id))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getNameComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.name == null)
? 1
: (qd2 == null || qd2.name == null)
? -1
: qd1.name.compareTo(qd2.name))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getStartTimeComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.startTime == null)
? 1
: (qd2 == null || qd2.startTime == null)
? -1
: qd1.startTime.compareTo(qd2.startTime))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getEndTimeComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.endTime == null)
? 1
: (qd2 == null || qd2.endTime == null)
? -1
: qd1.endTime.compareTo(qd2.endTime))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getComparator(final String sort) {
final boolean descending = PageSortOrder.getSortOrder(sort) == PageSortOrder.DESCENDING;
final String sortParam = PageSortOrder.decode(sort);
if (QUIZ_ATTR_NAME.equals(sortParam)) {
return getNameComparator(descending);
} else if (QUIZ_ATTR_START_TIME.equals(sortParam)) {
return getStartTimeComparator(descending);
} else if (QUIZ_ATTR_END_TIME.equals(sortParam)) {
return getEndTimeComparator(descending);
}
return getIdComparator(descending);
}
}
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.model.exam;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.util.Utils;
public final class QuizData implements GrantEntity {
public static final String FILTER_ATTR_START_TIME = "start_timestamp";
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "ADDITIONAL_ATTRIBUTES";
public static final String QUIZ_ATTR_ID = "quiz_id";
public static final String QUIZ_ATTR_INSTITUTION_ID = Domain.EXAM.ATTR_INSTITUTION_ID;
public static final String QUIZ_ATTR_LMS_SETUP_ID = "lms_setup_id";
public static final String QUIZ_ATTR_LMS_TYPE = "lms_setup_type";
public static final String QUIZ_ATTR_NAME = "quiz_name";
public static final String QUIZ_ATTR_DESCRIPTION = "quiz_description";
public static final String QUIZ_ATTR_START_TIME = "quiz_start_time";
public static final String QUIZ_ATTR_END_TIME = "quiz_end_time";
public static final String QUIZ_ATTR_START_URL = "quiz_start_url";
public static final String ATTR_ADDITIONAL_CREATION_TIME = "time_created";
public static final String ATTR_ADDITIONAL_SHORT_NAME = "course_short_name";
public static final String ATTR_ADDITIONAL_FULL_NAME = "course_full_name";
public static final String ATTR_ADDITIONAL_DISPLAY_NAME = "course_display_name";
public static final String ATTR_ADDITIONAL_SUMMARY = "course_summary";
public static final String ATTR_ADDITIONAL_TIME_LIMIT = "time_limit";
@JsonProperty(QUIZ_ATTR_ID)
public final String id;
@JsonProperty(QUIZ_ATTR_INSTITUTION_ID)
public final Long institutionId;
@JsonProperty(QUIZ_ATTR_LMS_SETUP_ID)
public final Long lmsSetupId;
@JsonProperty(QUIZ_ATTR_LMS_TYPE)
public final LmsType lmsType;
@JsonProperty(QUIZ_ATTR_NAME)
public final String name;
@JsonProperty(QUIZ_ATTR_DESCRIPTION)
public final String description;
@JsonProperty(QUIZ_ATTR_START_TIME)
public final DateTime startTime;
@JsonProperty(QUIZ_ATTR_END_TIME)
public final DateTime endTime;
@JsonProperty(QUIZ_ATTR_START_URL)
public final String startURL;
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES)
public final Map<String, String> additionalAttributes;
@JsonCreator
public QuizData(
@JsonProperty(QUIZ_ATTR_ID) final String id,
@JsonProperty(QUIZ_ATTR_INSTITUTION_ID) final Long institutionId,
@JsonProperty(QUIZ_ATTR_LMS_SETUP_ID) final Long lmsSetupId,
@JsonProperty(QUIZ_ATTR_LMS_TYPE) final LmsType lmsType,
@JsonProperty(QUIZ_ATTR_NAME) final String name,
@JsonProperty(QUIZ_ATTR_DESCRIPTION) final String description,
@JsonProperty(QUIZ_ATTR_START_TIME) final DateTime startTime,
@JsonProperty(QUIZ_ATTR_END_TIME) final DateTime endTime,
@JsonProperty(QUIZ_ATTR_START_URL) final String startURL,
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
this.id = id;
this.institutionId = institutionId;
this.lmsSetupId = lmsSetupId;
this.lmsType = lmsType;
this.name = name;
this.description = description;
this.startTime = startTime;
this.endTime = endTime;
this.startURL = startURL;
this.additionalAttributes = Utils.immutableMapOf(additionalAttributes);
}
public QuizData(
final String id,
final Long institutionId,
final Long lmsSetupId,
final LmsType lmsType,
final String name,
final String description,
final String startTime,
final String endTime,
final String startURL) {
this(id, institutionId, lmsSetupId, lmsType, name, description,
startTime, endTime, startURL, Collections.emptyMap());
}
public QuizData(
final String id,
final Long institutionId,
final Long lmsSetupId,
final LmsType lmsType,
final String name,
final String description,
final String startTime,
final String endTime,
final String startURL,
final Map<String, String> additionalAttributes) {
this.id = id;
this.institutionId = institutionId;
this.lmsSetupId = lmsSetupId;
this.lmsType = lmsType;
this.name = name;
this.description = description;
this.startTime = (startTime != null)
? LocalDateTime
.parse(startTime, Constants.STANDARD_DATE_TIME_FORMATTER)
.toDateTime(DateTimeZone.UTC)
: null;
this.endTime = (endTime != null)
? LocalDateTime
.parse(endTime, Constants.STANDARD_DATE_TIME_FORMATTER)
.toDateTime(DateTimeZone.UTC)
: null;
this.startURL = startURL;
this.additionalAttributes = Utils.immutableMapOf(additionalAttributes);
}
@Override
public String getModelId() {
if (this.id == null) {
return null;
}
return String.valueOf(this.id);
}
@Override
public EntityType entityType() {
return EntityType.EXAM;
}
public String geId() {
return this.id;
}
@Override
public Long getInstitutionId() {
return this.institutionId;
}
public Long getLmsSetupId() {
return this.lmsSetupId;
}
public LmsType getLmsType() {
return this.lmsType;
}
@Override
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
public DateTime getStartTime() {
return this.startTime;
}
public DateTime getEndTime() {
return this.endTime;
}
public String getStartURL() {
return this.startURL;
}
public Map<String, String> getAdditionalAttributes() {
return this.additionalAttributes;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("QuizData [id=");
builder.append(this.id);
builder.append(", institutionId=");
builder.append(this.institutionId);
builder.append(", lmsSetupId=");
builder.append(this.lmsSetupId);
builder.append(", lmsType=");
builder.append(this.lmsType);
builder.append(", name=");
builder.append(this.name);
builder.append(", description=");
builder.append(this.description);
builder.append(", startTime=");
builder.append(this.startTime);
builder.append(", endTime=");
builder.append(this.endTime);
builder.append(", startURL=");
builder.append(this.startURL);
builder.append("]");
return builder.toString();
}
public static Comparator<QuizData> getIdComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.id == null)
? 1
: (qd2 == null || qd2.id == null)
? -1
: qd1.id.compareTo(qd2.id))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getNameComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.name == null)
? 1
: (qd2 == null || qd2.name == null)
? -1
: qd1.name.compareTo(qd2.name))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getStartTimeComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.startTime == null)
? 1
: (qd2 == null || qd2.startTime == null)
? -1
: qd1.startTime.compareTo(qd2.startTime))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getEndTimeComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.endTime == null)
? 1
: (qd2 == null || qd2.endTime == null)
? -1
: qd1.endTime.compareTo(qd2.endTime))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getComparator(final String sort) {
final boolean descending = PageSortOrder.getSortOrder(sort) == PageSortOrder.DESCENDING;
final String sortParam = PageSortOrder.decode(sort);
if (QUIZ_ATTR_NAME.equals(sortParam)) {
return getNameComparator(descending);
} else if (QUIZ_ATTR_START_TIME.equals(sortParam)) {
return getStartTimeComparator(descending);
} else if (QUIZ_ATTR_END_TIME.equals(sortParam)) {
return getEndTimeComparator(descending);
}
return getIdComparator(descending);
}
}

View file

@ -332,7 +332,9 @@ public final class Utils {
return null;
}
return text.replace("</br>", "\n");
return text
.replace("<br/>", "\n")
.replace("<br></br>", "\n");
}
public static String encodeFormURL_UTF_8(final String value) {

View file

@ -1,231 +1,231 @@
/*
* 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;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.Charsets;
import org.apache.commons.codec.binary.Base64InputStream;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.client.RestTemplate;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService;
import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
@Lazy
@Component
@GuiProfile
public final class InstitutionalAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final String INST_SUFFIX_ATTRIBUTE = "instSuffix";
private static final Logger log = LoggerFactory.getLogger(InstitutionalAuthenticationEntryPoint.class);
private final String guiEntryPoint;
private final String defaultLogo;
private final WebserviceURIService webserviceURIService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
protected InstitutionalAuthenticationEntryPoint(
@Value("${sebserver.gui.entrypoint}") final String guiEntryPoint,
@Value("${sebserver.gui.defaultLogo:" + Constants.NO_NAME + "}") final String defaultLogoFileName,
final WebserviceURIService webserviceURIService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ResourceLoader resourceLoader) {
this.guiEntryPoint = guiEntryPoint;
this.webserviceURIService = webserviceURIService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
String _defaultLogo = null;
if (!Constants.NO_NAME.equals(defaultLogoFileName)) {
try {
final String extension = ImageUploadSelection.SUPPORTED_IMAGE_FILES.stream()
.filter(ext -> defaultLogoFileName.endsWith(ext))
.findFirst()
.orElse(null);
if (extension == null) {
throw new IllegalArgumentException("Image of type: " + defaultLogoFileName + " not supported");
}
final Resource resource = resourceLoader.getResource(defaultLogoFileName);
final Reader reader = new InputStreamReader(
new Base64InputStream(resource.getInputStream(), true),
Charsets.UTF_8);
_defaultLogo = FileCopyUtils.copyToString(reader);
} catch (final Exception e) {
log.warn("Failed to load default logo image from filesystem: {}", defaultLogoFileName);
_defaultLogo = null;
}
this.defaultLogo = _defaultLogo;
} else {
this.defaultLogo = null;
}
}
@Override
public void commence(
final HttpServletRequest request,
final HttpServletResponse response,
final AuthenticationException authException) throws IOException, ServletException {
final String institutionalEndpoint = extractInstitutionalEndpoint(request);
if (log.isDebugEnabled()) {
log.debug("No default gui entrypoint requested: {}", institutionalEndpoint);
}
final String logoImageBase64 = requestLogoImage(institutionalEndpoint);
if (StringUtils.isNotBlank(logoImageBase64)) {
request.getSession().setAttribute(API.PARAM_LOGO_IMAGE, logoImageBase64);
request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, institutionalEndpoint);
forwardToEntryPoint(request, response, this.guiEntryPoint);
} else {
request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
forwardToEntryPoint(request, response, this.guiEntryPoint);
}
}
private void forwardToEntryPoint(
final HttpServletRequest request,
final HttpServletResponse response,
final String entryPoint) throws ServletException, IOException {
final RequestDispatcher dispatcher = request
.getServletContext()
.getRequestDispatcher(entryPoint);
dispatcher.forward(request, response);
}
public static String extractInstitutionalEndpoint(final HttpServletRequest request) {
final String requestURI = request.getRequestURI();
if (log.isDebugEnabled()) {
log.debug("Trying to verify insitution from requested entrypoint url: {}", requestURI);
}
try {
return requestURI.substring(
requestURI.lastIndexOf(Constants.SLASH) + 1,
requestURI.length());
} catch (final Exception e) {
log.error("Fauled to extract institutional URL suffix: {}", e.getMessage());
return null;
}
}
public static String extractInstitutionalEndpoint() {
try {
final Object attribute = RWT.getUISession().getHttpSession().getAttribute(INST_SUFFIX_ATTRIBUTE);
return (attribute != null) ? String.valueOf(attribute) : null;
} catch (final Exception e) {
log.warn("Failed to extract institutional endpoint form user session: {}", e.getMessage());
return null;
}
}
private String requestLogoImage(final String institutionalEndpoint) {
if (StringUtils.isBlank(institutionalEndpoint)) {
return this.defaultLogo;
}
try {
final RestTemplate restTemplate = new RestTemplate();
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
.getClientHttpRequestFactory()
.getOrThrow();
restTemplate.setRequestFactory(clientHttpRequestFactory);
final ResponseEntity<String> exchange = restTemplate
.exchange(
this.webserviceURIService.getURIBuilder()
.path(API.INFO_ENDPOINT + API.INSTITUTIONAL_LOGO_PATH)
.toUriString(),
HttpMethod.GET,
HttpEntity.EMPTY,
String.class,
institutionalEndpoint);
if (exchange.getStatusCodeValue() == HttpStatus.OK.value()) {
return exchange.getBody();
} else {
log.warn("Failed to verify insitution from requested entrypoint url: {}, response: {}",
institutionalEndpoint,
exchange);
}
} catch (final Exception e) {
log.warn("Failed to verify insitution from requested entrypoint url: {}",
institutionalEndpoint,
e);
}
return null;
}
/** TODO this seems not to work as expected. Different Theme is only possible in RAP on different
* entry-points and since entry-points are statically defined within the RAPConficuration
* there is no possibility to apply them dynamically within an institution so far.
*
* @param institutionalEndpoint
* @return */
// private boolean initInstitutionalBasedThemeEntryPoint(final String institutionalEndpoint) {
// try {
// final ApplicationContextImpl appContext = (ApplicationContextImpl) RWT.getApplicationContext();
// final Map<String, String> properties = new HashMap<>();
// properties.put(WebClient.THEME_ID, "sms");
// appContext.getEntryPointManager().register(
// institutionalEndpoint,
// new RAPSpringEntryPointFactory(),
// properties);
//
// return true;
// } catch (final Exception e) {
// log.warn("Failed to dynamically set entry point for institution: {}", institutionalEndpoint, e);
// return false;
// }
// }
/*
* 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;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.Charsets;
import org.apache.commons.codec.binary.Base64InputStream;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.client.RestTemplate;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService;
import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
@Lazy
@Component
@GuiProfile
public final class InstitutionalAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final String INST_SUFFIX_ATTRIBUTE = "instSuffix";
private static final Logger log = LoggerFactory.getLogger(InstitutionalAuthenticationEntryPoint.class);
private final String guiEntryPoint;
private final String defaultLogo;
private final WebserviceURIService webserviceURIService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
protected InstitutionalAuthenticationEntryPoint(
@Value("${sebserver.gui.entrypoint}") final String guiEntryPoint,
@Value("${sebserver.gui.defaultLogo:" + Constants.NO_NAME + "}") final String defaultLogoFileName,
final WebserviceURIService webserviceURIService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ResourceLoader resourceLoader) {
this.guiEntryPoint = guiEntryPoint;
this.webserviceURIService = webserviceURIService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
String _defaultLogo = null;
if (!Constants.NO_NAME.equals(defaultLogoFileName)) {
try {
final String extension = ImageUploadSelection.SUPPORTED_IMAGE_FILES.stream()
.filter(ext -> defaultLogoFileName.endsWith(ext))
.findFirst()
.orElse(null);
if (extension == null) {
throw new IllegalArgumentException("Image of type: " + defaultLogoFileName + " not supported");
}
final Resource resource = resourceLoader.getResource(defaultLogoFileName);
final Reader reader = new InputStreamReader(
new Base64InputStream(resource.getInputStream(), true),
Charsets.UTF_8);
_defaultLogo = FileCopyUtils.copyToString(reader);
} catch (final Exception e) {
log.warn("Failed to load default logo image from filesystem: {}", defaultLogoFileName);
_defaultLogo = null;
}
this.defaultLogo = _defaultLogo;
} else {
this.defaultLogo = null;
}
}
@Override
public void commence(
final HttpServletRequest request,
final HttpServletResponse response,
final AuthenticationException authException) throws IOException, ServletException {
final String institutionalEndpoint = extractInstitutionalEndpoint(request);
if (log.isDebugEnabled()) {
log.debug("No default gui entrypoint requested: {}", institutionalEndpoint);
}
final String logoImageBase64 = requestLogoImage(institutionalEndpoint);
if (StringUtils.isNotBlank(logoImageBase64)) {
request.getSession().setAttribute(API.PARAM_LOGO_IMAGE, logoImageBase64);
request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, institutionalEndpoint);
forwardToEntryPoint(request, response, this.guiEntryPoint);
} else {
request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
forwardToEntryPoint(request, response, this.guiEntryPoint);
}
}
private void forwardToEntryPoint(
final HttpServletRequest request,
final HttpServletResponse response,
final String entryPoint) throws ServletException, IOException {
final RequestDispatcher dispatcher = request
.getServletContext()
.getRequestDispatcher(entryPoint);
dispatcher.forward(request, response);
}
public static String extractInstitutionalEndpoint(final HttpServletRequest request) {
final String requestURI = request.getRequestURI();
if (log.isDebugEnabled()) {
log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI);
}
try {
return requestURI.substring(
requestURI.lastIndexOf(Constants.SLASH) + 1,
requestURI.length());
} catch (final Exception e) {
log.error("Failed to extract institutional URL suffix: {}", e.getMessage());
return null;
}
}
public static String extractInstitutionalEndpoint() {
try {
final Object attribute = RWT.getUISession().getHttpSession().getAttribute(INST_SUFFIX_ATTRIBUTE);
return (attribute != null) ? String.valueOf(attribute) : null;
} catch (final Exception e) {
log.warn("Failed to extract institutional endpoint form user session: {}", e.getMessage());
return null;
}
}
private String requestLogoImage(final String institutionalEndpoint) {
if (StringUtils.isBlank(institutionalEndpoint)) {
return this.defaultLogo;
}
try {
final RestTemplate restTemplate = new RestTemplate();
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
.getClientHttpRequestFactory()
.getOrThrow();
restTemplate.setRequestFactory(clientHttpRequestFactory);
final ResponseEntity<String> exchange = restTemplate
.exchange(
this.webserviceURIService.getURIBuilder()
.path(API.INFO_ENDPOINT + API.INSTITUTIONAL_LOGO_PATH)
.toUriString(),
HttpMethod.GET,
HttpEntity.EMPTY,
String.class,
institutionalEndpoint);
if (exchange.getStatusCodeValue() == HttpStatus.OK.value()) {
return exchange.getBody();
} else {
log.warn("Failed to verify insitution from requested entrypoint url: {}, response: {}",
institutionalEndpoint,
exchange);
}
} catch (final Exception e) {
log.warn("Failed to verify insitution from requested entrypoint url: {}",
institutionalEndpoint,
e);
}
return null;
}
/** TODO this seems not to work as expected. Different Theme is only possible in RAP on different
* entry-points and since entry-points are statically defined within the RAPConficuration
* there is no possibility to apply them dynamically within an institution so far.
*
* @param institutionalEndpoint
* @return */
// private boolean initInstitutionalBasedThemeEntryPoint(final String institutionalEndpoint) {
// try {
// final ApplicationContextImpl appContext = (ApplicationContextImpl) RWT.getApplicationContext();
// final Map<String, String> properties = new HashMap<>();
// properties.put(WebClient.THEME_ID, "sms");
// appContext.getEntryPointManager().register(
// institutionalEndpoint,
// new RAPSpringEntryPointFactory(),
// properties);
//
// return true;
// } catch (final Exception e) {
// log.warn("Failed to dynamically set entry point for institution: {}", institutionalEndpoint, e);
// return false;
// }
// }
}

View file

@ -14,6 +14,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
@ -117,6 +118,8 @@ public class ExamForm implements TemplateComposer {
private final static LocTextKey CONFIG_LIST_TITLE_KEY =
new LocTextKey("sebserver.exam.configuration.list.title");
private final static LocTextKey CONFIG_LIST_TITLE_TOOLTIP_KEY =
new LocTextKey("sebserver.exam.configuration.list.title" + Constants.TOOLTIP_TEXT_KEY_SUFFIX);
private final static LocTextKey CONFIG_NAME_COLUMN_KEY =
new LocTextKey("sebserver.exam.configuration.list.column.name");
private final static LocTextKey CONFIG_DESCRIPTION_COLUMN_KEY =
@ -128,6 +131,8 @@ public class ExamForm implements TemplateComposer {
private final static LocTextKey INDICATOR_LIST_TITLE_KEY =
new LocTextKey("sebserver.exam.indicator.list.title");
private final static LocTextKey INDICATOR_LIST_TITLE_TOOLTIP_KEY =
new LocTextKey("sebserver.exam.indicator.list.title" + Constants.TOOLTIP_TEXT_KEY_SUFFIX);
private final static LocTextKey INDICATOR_TYPE_COLUMN_KEY =
new LocTextKey("sebserver.exam.indicator.list.column.type");
private final static LocTextKey INDICATOR_NAME_COLUMN_KEY =
@ -326,11 +331,12 @@ public class ExamForm implements TemplateComposer {
.addField(FormBuilder.singleSelection(
Domain.EXAM.ATTR_TYPE,
FORM_TYPE_TEXT_KEY,
String.valueOf(exam.type),
(exam.type != null) ? String.valueOf(exam.type) : Exam.ExamType.UNDEFINED.name(),
this.resourceService::examTypeResources)
.withLabelSpan(2)
.withInputSpan(4)
.withEmptyCellSpan(2))
.withEmptyCellSpan(2)
.mandatory(!readonly))
.addField(FormBuilder.multiComboSelection(
Domain.EXAM.ATTR_SUPPORTER,
@ -339,7 +345,8 @@ public class ExamForm implements TemplateComposer {
this.resourceService::examSupporterResources)
.withLabelSpan(2)
.withInputSpan(4)
.withEmptyCellSpan(2))
.withEmptyCellSpan(2)
.mandatory(!readonly))
.buildFor(importFromQuizData
? this.restService.getRestCall(ImportAsExam.class)
@ -398,7 +405,8 @@ public class ExamForm implements TemplateComposer {
this.widgetFactory.labelLocalized(
content,
CustomVariant.TEXT_H3,
CONFIG_LIST_TITLE_KEY);
CONFIG_LIST_TITLE_KEY,
CONFIG_LIST_TITLE_TOOLTIP_KEY);
this.widgetFactory.labelSeparator(content);
final EntityTable<ExamConfigurationMap> configurationTable =
@ -428,6 +436,13 @@ public class ExamForm implements TemplateComposer {
() -> modifyGrant,
this::viewExamConfigPageAction)
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP,
ActionDefinition.EXAM_CONFIGURATION_DELETE_FROM_LIST,
ActionDefinition.EXAM_CONFIGURATION_EXPORT,
ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY))
.compose(pageContext.copyOf(content));
final EntityKey configMapKey = (configurationTable.hasAnyContent())
@ -447,7 +462,7 @@ public class ExamForm implements TemplateComposer {
.newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP)
.withParentEntityKey(entityKey)
.withEntityKey(configMapKey)
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent())
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_DELETE_FROM_LIST)
.withEntityKey(entityKey)
@ -461,7 +476,7 @@ public class ExamForm implements TemplateComposer {
}
return null;
})
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable)
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable, false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_EXPORT)
.withParentEntityKey(entityKey)
@ -470,7 +485,7 @@ public class ExamForm implements TemplateComposer {
this::downloadExamConfigAction,
CONFIG_EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(() -> userGrantCheck.r() && configurationTable.hasAnyContent())
.publishIf(() -> userGrantCheck.r() && configurationTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY)
.withSelect(
@ -478,14 +493,15 @@ public class ExamForm implements TemplateComposer {
this::getExamConfigKey,
CONFIG_EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(() -> userGrantCheck.r() && configurationTable.hasAnyContent());
.publishIf(() -> userGrantCheck.r() && configurationTable.hasAnyContent(), false);
// List of Indicators
this.widgetFactory.label(content, "");
this.widgetFactory.label(content, StringUtils.EMPTY);
this.widgetFactory.labelLocalized(
content,
CustomVariant.TEXT_H3,
INDICATOR_LIST_TITLE_KEY);
INDICATOR_LIST_TITLE_KEY,
INDICATOR_LIST_TITLE_TOOLTIP_KEY);
this.widgetFactory.labelSeparator(content);
final EntityTable<Indicator> indicatorTable =
@ -520,6 +536,11 @@ public class ExamForm implements TemplateComposer {
.withParentEntityKey(entityKey)
.create())
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.EXAM_INDICATOR_MODIFY_FROM_LIST,
ActionDefinition.EXAM_INDICATOR_DELETE_FROM_LIST))
.compose(pageContext.copyOf(content));
actionBuilder
@ -534,7 +555,7 @@ public class ExamForm implements TemplateComposer {
indicatorTable::getSelection,
PageAction::applySingleSelectionAsEntityKey,
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent())
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_INDICATOR_DELETE_FROM_LIST)
.withEntityKey(entityKey)
@ -542,7 +563,7 @@ public class ExamForm implements TemplateComposer {
indicatorTable::getSelection,
this::deleteSelectedIndicator,
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent());
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent(), false);
}
}
@ -560,7 +581,7 @@ public class ExamForm implements TemplateComposer {
processFormSave,
true,
this.restService,
t -> log.error("Failed to intially restrict the course for SEB on LMS: {}", t.getMessage()));
t -> log.error("Failed to initially restrict the course for SEB on LMS: {}", t.getMessage()));
}
return processFormSave;
@ -589,7 +610,7 @@ public class ExamForm implements TemplateComposer {
result
.stream()
.map(message -> this.consistencyMessageMapping.get(message.messageCode))
.filter(message -> message != null)
.filter(Objects::nonNull)
.forEach(message -> this.widgetFactory.labelLocalized(
warningPanel,
CustomVariant.MESSAGE,
@ -699,7 +720,7 @@ public class ExamForm implements TemplateComposer {
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.withQueryParam(QuizData.QUIZ_ATTR_LMS_SETUP_ID, parentEntityKey.modelId)
.call()
.map(quizzData -> new Exam(quizzData))
.map(Exam::new)
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error));
}
@ -735,7 +756,7 @@ public class ExamForm implements TemplateComposer {
.append(")")
.append("</span>")
.append(" | "),
(sb1, sb2) -> sb1.append(sb2));
StringBuilder::append);
builder.delete(builder.length() - 3, builder.length() - 1);
return builder.toString();
}

View file

@ -1,279 +1,277 @@
/*
* 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 java.util.function.BiConsumer;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.TableItem;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
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.PageMessageException;
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.exam.CheckExamConsistency;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy
@Component
@GuiProfile
public class ExamList implements TemplateComposer {
static final String EXAM_LIST_COLUMN_STARTTIME =
"sebserver.exam.list.column.starttime";
static final LocTextKey PAGE_TITLE_KEY =
new LocTextKey("sebserver.exam.list.title");
static final LocTextKey NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION =
new LocTextKey("sebserver.exam.list.action.no.modify.privilege");
final static LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.exam.info.pleaseSelect");
final static LocTextKey COLUMN_TITLE_INSTITUTION_KEY =
new LocTextKey("sebserver.exam.list.column.institution");
final static LocTextKey COLUMN_TITLE_LMS_KEY =
new LocTextKey("sebserver.exam.list.column.lmssetup");
final static LocTextKey COLUMN_TITLE_NAME_KEY =
new LocTextKey("sebserver.exam.list.column.name");
final static LocTextKey COLUMN_TITLE_TYPE_KEY =
new LocTextKey("sebserver.exam.list.column.type");
final static LocTextKey NO_MODIFY_OF_OUT_DATED_EXAMS =
new LocTextKey("sebserver.exam.list.modify.out.dated");
final static LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.exam.list.empty");
private final TableFilterAttribute institutionFilter;
private final TableFilterAttribute lmsFilter;
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
private final TableFilterAttribute typeFilter;
private final PageService pageService;
private final ResourceService resourceService;
private final int pageSize;
protected ExamList(
final PageService pageService,
final ResourceService resourceService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.resourceService = resourceService;
this.pageSize = pageSize;
this.institutionFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Entity.FILTER_ATTR_INSTITUTION,
this.resourceService::institutionResource);
this.lmsFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
LmsSetup.FILTER_ATTR_LMS_SETUP,
this.resourceService::lmsSetupResource);
this.typeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Exam.FILTER_ATTR_TYPE,
this.resourceService::examTypeResources);
}
@Override
public void compose(final PageContext pageContext) {
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(),
PAGE_TITLE_KEY);
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final BooleanSupplier isSebAdmin =
() -> currentUser.get().hasRole(UserRole.SEB_SERVER_ADMIN);
final Function<String, String> institutionNameFunction =
this.resourceService.getInstitutionNameFunction();
// table
final EntityTable<Exam> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetExamPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withRowDecorator(decorateOnExamConsistency(this.pageService))
.withStaticFilter(Exam.FILTER_ATTR_ACTIVE, Constants.TRUE_STRING)
.withColumnIf(
isSebAdmin,
() -> new ColumnDefinition<Exam>(
Domain.EXAM.ATTR_INSTITUTION_ID,
COLUMN_TITLE_INSTITUTION_KEY,
exam -> institutionNameFunction
.apply(String.valueOf(exam.getInstitutionId())))
.withFilter(this.institutionFilter))
.withColumn(new ColumnDefinition<>(
Domain.EXAM.ATTR_LMS_SETUP_ID,
COLUMN_TITLE_LMS_KEY,
examLmsSetupNameFunction(this.resourceService))
.withFilter(this.lmsFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_NAME,
COLUMN_TITLE_NAME_KEY,
Exam::getName)
.withFilter(this.nameFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_START_TIME,
new LocTextKey(
EXAM_LIST_COLUMN_STARTTIME,
i18nSupport.getUsersTimeZoneTitleSuffix()),
Exam::getStartTime)
.withFilter(new TableFilterAttribute(
CriteriaType.DATE,
QuizData.FILTER_ATTR_START_TIME,
Utils.toDateTimeUTC(Utils.getMillisecondsNow())
.minusYears(1)
.toString()))
.sortable())
.withColumn(new ColumnDefinition<Exam>(
Domain.EXAM.ATTR_TYPE,
COLUMN_TITLE_TYPE_KEY,
this.resourceService::localizedExamTypeName)
.withFilter(this.typeFilter)
.sortable())
.withDefaultAction(actionBuilder
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)
.create())
.compose(pageContext.copyOf(content));
// propagate content actions to action-pane
final GrantCheck userGrant = currentUser.grantCheck(EntityType.EXAM);
actionBuilder
// Removed as discussed in SEBSERV-52
// .newAction(ActionDefinition.EXAM_IMPORT)
// .publishIf(userGrant::im)
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.publishIf(table::hasAnyContent)
.newAction(ActionDefinition.EXAM_MODIFY_FROM_LIST)
.withSelect(
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION),
action -> modifyExam(action, table),
EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im() && table.hasAnyContent());
}
static final PageAction modifyExam(final PageAction action, final EntityTable<Exam> table) {
final Exam exam = table.getSingleSelectedROWData();
if (exam == null) {
throw new PageMessageException(EMPTY_SELECTION_TEXT_KEY);
}
if (exam.endTime != null) {
final DateTime now = DateTime.now(DateTimeZone.UTC);
if (exam.endTime.isBefore(now)) {
throw new PageMessageException(NO_MODIFY_OF_OUT_DATED_EXAMS);
}
}
return action.withEntityKey(action.getSingleSelection());
}
static final BiConsumer<TableItem, ExamConfigurationMap> decorateOnExamMapConsistency(
final PageService pageService) {
return (item, examMap) -> {
pageService.getRestService().getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(examMap.examId))
.call()
.ifPresent(exam -> decorateOnExamConsistency(item, exam, pageService));
};
}
static final BiConsumer<TableItem, Exam> decorateOnExamConsistency(final PageService pageService) {
return (item, exam) -> decorateOnExamConsistency(item, exam, pageService);
}
static final void decorateOnExamConsistency(
final TableItem item,
final Exam exam,
final PageService pageService) {
if (exam.getStatus() != ExamStatus.RUNNING) {
return;
}
pageService.getRestService().getBuilder(CheckExamConsistency.class)
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId())
.call()
.ifPresent(warnings -> {
if (warnings != null && !warnings.isEmpty()) {
item.setData(RWT.CUSTOM_VARIANT, CustomVariant.WARNING.key);
}
});
}
private static Function<Exam, String> examLmsSetupNameFunction(final ResourceService resourceService) {
return exam -> resourceService.getLmsSetupNameFunction()
.apply(String.valueOf(exam.lmsSetupId));
}
}
/*
* 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 java.util.function.BiConsumer;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.TableItem;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
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.PageMessageException;
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.exam.CheckExamConsistency;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy
@Component
@GuiProfile
public class ExamList implements TemplateComposer {
static final String EXAM_LIST_COLUMN_STARTTIME =
"sebserver.exam.list.column.starttime";
static final LocTextKey PAGE_TITLE_KEY =
new LocTextKey("sebserver.exam.list.title");
static final LocTextKey NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION =
new LocTextKey("sebserver.exam.list.action.no.modify.privilege");
final static LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.exam.info.pleaseSelect");
final static LocTextKey COLUMN_TITLE_INSTITUTION_KEY =
new LocTextKey("sebserver.exam.list.column.institution");
final static LocTextKey COLUMN_TITLE_LMS_KEY =
new LocTextKey("sebserver.exam.list.column.lmssetup");
final static LocTextKey COLUMN_TITLE_NAME_KEY =
new LocTextKey("sebserver.exam.list.column.name");
final static LocTextKey COLUMN_TITLE_TYPE_KEY =
new LocTextKey("sebserver.exam.list.column.type");
final static LocTextKey NO_MODIFY_OF_OUT_DATED_EXAMS =
new LocTextKey("sebserver.exam.list.modify.out.dated");
final static LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.exam.list.empty");
private final TableFilterAttribute institutionFilter;
private final TableFilterAttribute lmsFilter;
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
private final TableFilterAttribute typeFilter;
private final PageService pageService;
private final ResourceService resourceService;
private final int pageSize;
protected ExamList(
final PageService pageService,
final ResourceService resourceService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.resourceService = resourceService;
this.pageSize = pageSize;
this.institutionFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Entity.FILTER_ATTR_INSTITUTION,
this.resourceService::institutionResource);
this.lmsFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
LmsSetup.FILTER_ATTR_LMS_SETUP,
this.resourceService::lmsSetupResource);
this.typeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Exam.FILTER_ATTR_TYPE,
this.resourceService::examTypeResources);
}
@Override
public void compose(final PageContext pageContext) {
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(),
PAGE_TITLE_KEY);
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final BooleanSupplier isSebAdmin =
() -> currentUser.get().hasRole(UserRole.SEB_SERVER_ADMIN);
final Function<String, String> institutionNameFunction =
this.resourceService.getInstitutionNameFunction();
// table
final EntityTable<Exam> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetExamPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withRowDecorator(decorateOnExamConsistency(this.pageService))
.withStaticFilter(Exam.FILTER_ATTR_ACTIVE, Constants.TRUE_STRING)
.withColumnIf(
isSebAdmin,
() -> new ColumnDefinition<Exam>(
Domain.EXAM.ATTR_INSTITUTION_ID,
COLUMN_TITLE_INSTITUTION_KEY,
exam -> institutionNameFunction
.apply(String.valueOf(exam.getInstitutionId())))
.withFilter(this.institutionFilter))
.withColumn(new ColumnDefinition<>(
Domain.EXAM.ATTR_LMS_SETUP_ID,
COLUMN_TITLE_LMS_KEY,
examLmsSetupNameFunction(this.resourceService))
.withFilter(this.lmsFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_NAME,
COLUMN_TITLE_NAME_KEY,
Exam::getName)
.withFilter(this.nameFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_START_TIME,
new LocTextKey(
EXAM_LIST_COLUMN_STARTTIME,
i18nSupport.getUsersTimeZoneTitleSuffix()),
Exam::getStartTime)
.withFilter(new TableFilterAttribute(
CriteriaType.DATE,
QuizData.FILTER_ATTR_START_TIME,
Utils.toDateTimeUTC(Utils.getMillisecondsNow())
.minusYears(1)
.toString()))
.sortable())
.withColumn(new ColumnDefinition<Exam>(
Domain.EXAM.ATTR_TYPE,
COLUMN_TITLE_TYPE_KEY,
this.resourceService::localizedExamTypeName)
.withFilter(this.typeFilter)
.sortable())
.withDefaultAction(actionBuilder
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)
.create())
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.EXAM_VIEW_FROM_LIST,
ActionDefinition.EXAM_MODIFY_FROM_LIST))
.compose(pageContext.copyOf(content));
// propagate content actions to action-pane
final GrantCheck userGrant = currentUser.grantCheck(EntityType.EXAM);
actionBuilder
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.publishIf(table::hasAnyContent, false)
.newAction(ActionDefinition.EXAM_MODIFY_FROM_LIST)
.withSelect(
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION),
action -> modifyExam(action, table),
EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im() && table.hasAnyContent(), false);
}
static PageAction modifyExam(final PageAction action, final EntityTable<Exam> table) {
final Exam exam = table.getSingleSelectedROWData();
if (exam == null) {
throw new PageMessageException(EMPTY_SELECTION_TEXT_KEY);
}
if (exam.endTime != null) {
final DateTime now = DateTime.now(DateTimeZone.UTC);
if (exam.endTime.isBefore(now)) {
throw new PageMessageException(NO_MODIFY_OF_OUT_DATED_EXAMS);
}
}
return action.withEntityKey(action.getSingleSelection());
}
static BiConsumer<TableItem, ExamConfigurationMap> decorateOnExamMapConsistency(
final PageService pageService) {
return (item, examMap) -> pageService.getRestService().getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(examMap.examId))
.call()
.ifPresent(exam -> decorateOnExamConsistency(item, exam, pageService));
}
static BiConsumer<TableItem, Exam> decorateOnExamConsistency(final PageService pageService) {
return (item, exam) -> decorateOnExamConsistency(item, exam, pageService);
}
static void decorateOnExamConsistency(
final TableItem item,
final Exam exam,
final PageService pageService) {
if (exam.getStatus() != ExamStatus.RUNNING) {
return;
}
pageService.getRestService().getBuilder(CheckExamConsistency.class)
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId())
.call()
.ifPresent(warnings -> {
if (warnings != null && !warnings.isEmpty()) {
item.setData(RWT.CUSTOM_VARIANT, CustomVariant.WARNING.key);
}
});
}
private static Function<Exam, String> examLmsSetupNameFunction(final ResourceService resourceService) {
return exam -> resourceService.getLmsSetupNameFunction()
.apply(String.valueOf(exam.lmsSetupId));
}
}

View file

@ -1,285 +1,297 @@
/*
* 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.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
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.widgets.Composite;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSebRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.SebRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ActivateSebRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeactivateSebRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetSebRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveSebRestriction;
public class ExamSebRestrictionSettings {
private final static LocTextKey SEB_RESTRICTION_ERROR =
new LocTextKey("sebserver.error.exam.seb.restriction");
private final static LocTextKey SEB_RESTRICTION_FORM_TITLE =
new LocTextKey("sebserver.exam.action.sebrestriction.details");
private final static LocTextKey SEB_RESTRICTION_FORM_CONFIG_KEYS =
new LocTextKey("sebserver.exam.form.sebrestriction.configKeys");
private final static LocTextKey SEB_RESTRICTION_FORM_BROWSER_KEYS =
new LocTextKey("sebserver.exam.form.sebrestriction.browserExamKeys");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_WHITE_LIST_PATHS =
new LocTextKey("sebserver.exam.form.sebrestriction.WHITELIST_PATHS");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_PERMISSIONS =
new LocTextKey("sebserver.exam.form.sebrestriction.PERMISSION_COMPONENTS");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_BLACKLIST_CHAPTERS =
new LocTextKey("sebserver.exam.form.sebrestriction.BLACKLIST_CHAPTERS");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_USER_BANNING_ENABLED =
new LocTextKey("sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED");
static final String PAGE_CONTEXT_ATTR_LMS_TYPE = "ATTR_LMS_TYPE";
static Function<PageAction, PageAction> settingsFunction(final PageService pageService) {
return action -> {
final PageContext pageContext = action.pageContext();
final ModalInputDialog<FormHandle<?>> dialog =
new ModalInputDialog<FormHandle<?>>(
action.pageContext().getParent().getShell(),
pageService.getWidgetFactory())
.setDialogWidth(740)
.setDialogHeight(400);
final SebRestrictionPropertiesForm bindFormContext = new SebRestrictionPropertiesForm(
pageService,
action.pageContext());
final Predicate<FormHandle<?>> doBind = formHandle -> doCreate(
pageService,
pageContext,
formHandle);
dialog.open(
SEB_RESTRICTION_FORM_TITLE,
doBind,
Utils.EMPTY_EXECUTION,
bindFormContext);
return action;
};
}
private static final boolean doCreate(
final PageService pageService,
final PageContext pageContext,
final FormHandle<?> formHandle) {
final EntityKey entityKey = pageContext.getEntityKey();
final LmsType lmsType = getLmsType(pageContext);
SebRestriction bodyValue = null;
try {
final Form form = formHandle.getForm();
final Collection<String> browserKeys = Utils.getListOfLines(
form.getFieldValue(SebRestriction.ATTR_BROWSER_KEYS));
final Map<String, String> additionalAttributes = new HashMap<>();
if (lmsType == LmsType.OPEN_EDX) {
additionalAttributes.put(
OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS,
form.getFieldValue(OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS));
additionalAttributes.put(
OpenEdxSebRestriction.ATTR_WHITELIST_PATHS,
form.getFieldValue(OpenEdxSebRestriction.ATTR_WHITELIST_PATHS));
additionalAttributes.put(
OpenEdxSebRestriction.ATTR_USER_BANNING_ENABLED,
form.getFieldValue(OpenEdxSebRestriction.ATTR_USER_BANNING_ENABLED));
additionalAttributes.put(
OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS,
Utils.convertCarriageReturnToListSeparator(
form.getFieldValue(OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS)));
}
bodyValue = new SebRestriction(
Long.parseLong(entityKey.modelId),
null,
browserKeys,
additionalAttributes);
} catch (final Exception e) {
e.printStackTrace();
}
return !pageService
.getRestService()
.getBuilder(SaveSebRestriction.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.withBody(bodyValue)
.call()
.onError(formHandle::handleError)
.hasError();
}
private static final class SebRestrictionPropertiesForm
implements ModalInputDialogComposer<FormHandle<?>> {
private final PageService pageService;
private final PageContext pageContext;
protected SebRestrictionPropertiesForm(
final PageService pageService,
final PageContext pageContext) {
this.pageService = pageService;
this.pageContext = pageContext;
}
@Override
public Supplier<FormHandle<?>> compose(final Composite parent) {
final RestService restService = this.pageService.getRestService();
final ResourceService resourceService = this.pageService.getResourceService();
final EntityKey entityKey = this.pageContext.getEntityKey();
final LmsType lmsType = getLmsType(this.pageContext);
final Composite content = this.pageService
.getWidgetFactory()
.createPopupScrollComposite(parent);
final SebRestriction sebRestriction = restService
.getBuilder(GetSebRestriction.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.getOrThrow();
final PageContext formContext = this.pageContext
.copyOf(content)
.clearEntityKeys();
final FormHandle<SebRestriction> formHandle = this.pageService.formBuilder(
formContext)
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(false)
.addField(FormBuilder.text(
SebRestriction.ATTR_CONFIG_KEYS,
SEB_RESTRICTION_FORM_CONFIG_KEYS,
StringUtils.join(sebRestriction.getConfigKeys(), Constants.CARRIAGE_RETURN))
.asArea(50)
.readonly(true))
.addField(FormBuilder.text(
SebRestriction.ATTR_BROWSER_KEYS,
SEB_RESTRICTION_FORM_BROWSER_KEYS,
StringUtils.join(sebRestriction.getBrowserExamKeys(), Constants.CARRIAGE_RETURN))
.asArea())
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.multiCheckboxSelection(
OpenEdxSebRestriction.ATTR_WHITELIST_PATHS,
SEB_RESTRICTION_FORM_EDX_WHITE_LIST_PATHS,
sebRestriction.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_WHITELIST_PATHS),
() -> resourceService.sebRestrictionWhiteListResources()))
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.multiCheckboxSelection(
OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS,
SEB_RESTRICTION_FORM_EDX_PERMISSIONS,
sebRestriction.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS),
() -> resourceService.sebRestrictionPermissionResources()))
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.text(
OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS,
SEB_RESTRICTION_FORM_EDX_BLACKLIST_CHAPTERS,
Utils.convertListSeparatorToCarriageReturn(
sebRestriction
.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS)))
.asArea())
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.checkbox(
OpenEdxSebRestriction.ATTR_USER_BANNING_ENABLED,
SEB_RESTRICTION_FORM_EDX_USER_BANNING_ENABLED,
sebRestriction
.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_USER_BANNING_ENABLED)))
.build();
return () -> formHandle;
}
}
private static LmsType getLmsType(final PageContext pageContext) {
try {
return LmsType.valueOf(pageContext.getAttribute(PAGE_CONTEXT_ATTR_LMS_TYPE));
} catch (final Exception e) {
return null;
}
}
public static PageAction setSebRestriction(
final PageAction action,
final boolean activateRestriction,
final RestService restService) {
return setSebRestriction(
action,
activateRestriction,
restService,
error -> action.pageContext().notifyError(SEB_RESTRICTION_ERROR, error));
}
public static PageAction setSebRestriction(
final PageAction action,
final boolean activateRestriction,
final RestService restService,
final Consumer<Exception> errorHandler) {
restService.getBuilder((activateRestriction)
? ActivateSebRestriction.class
: DeactivateSebRestriction.class)
.withURIVariable(
API.PARAM_MODEL_ID,
action.getEntityKey().modelId)
.call()
.onError(errorHandler);
return action;
}
}
/*
* 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.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
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.widgets.Composite;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSebRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.SebRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ActivateSebRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeactivateSebRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetSebRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveSebRestriction;
public class ExamSebRestrictionSettings {
private final static LocTextKey SEB_RESTRICTION_ERROR =
new LocTextKey("sebserver.error.exam.seb.restriction");
private final static LocTextKey SEB_RESTRICTION_FORM_TITLE =
new LocTextKey("sebserver.exam.action.sebrestriction.details");
private final static LocTextKey SEB_RESTRICTION_FORM_INFO =
new LocTextKey("sebserver.exam.form.sebrestriction.info");
private final static LocTextKey SEB_RESTRICTION_FORM_INFO_TEXT =
new LocTextKey("sebserver.exam.form.sebrestriction.info-text");
private final static LocTextKey SEB_RESTRICTION_FORM_CONFIG_KEYS =
new LocTextKey("sebserver.exam.form.sebrestriction.configKeys");
private final static LocTextKey SEB_RESTRICTION_FORM_BROWSER_KEYS =
new LocTextKey("sebserver.exam.form.sebrestriction.browserExamKeys");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_WHITE_LIST_PATHS =
new LocTextKey("sebserver.exam.form.sebrestriction.WHITELIST_PATHS");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_PERMISSIONS =
new LocTextKey("sebserver.exam.form.sebrestriction.PERMISSION_COMPONENTS");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_BLACKLIST_CHAPTERS =
new LocTextKey("sebserver.exam.form.sebrestriction.BLACKLIST_CHAPTERS");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_USER_BANNING_ENABLED =
new LocTextKey("sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED");
static final String PAGE_CONTEXT_ATTR_LMS_TYPE = "ATTR_LMS_TYPE";
static Function<PageAction, PageAction> settingsFunction(final PageService pageService) {
return action -> {
final PageContext pageContext = action.pageContext();
final ModalInputDialog<FormHandle<?>> dialog =
new ModalInputDialog<FormHandle<?>>(
action.pageContext().getParent().getShell(),
pageService.getWidgetFactory())
.setDialogWidth(740)
.setDialogHeight(400);
final SebRestrictionPropertiesForm bindFormContext = new SebRestrictionPropertiesForm(
pageService,
action.pageContext());
final Predicate<FormHandle<?>> doBind = formHandle -> doCreate(
pageService,
pageContext,
formHandle);
dialog.open(
SEB_RESTRICTION_FORM_TITLE,
doBind,
Utils.EMPTY_EXECUTION,
bindFormContext);
return action;
};
}
private static boolean doCreate(
final PageService pageService,
final PageContext pageContext,
final FormHandle<?> formHandle) {
final EntityKey entityKey = pageContext.getEntityKey();
final LmsType lmsType = getLmsType(pageContext);
SebRestriction bodyValue = null;
try {
final Form form = formHandle.getForm();
final Collection<String> browserKeys = Utils.getListOfLines(
form.getFieldValue(SebRestriction.ATTR_BROWSER_KEYS));
final Map<String, String> additionalAttributes = new HashMap<>();
if (lmsType == LmsType.OPEN_EDX) {
additionalAttributes.put(
OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS,
form.getFieldValue(OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS));
additionalAttributes.put(
OpenEdxSebRestriction.ATTR_WHITELIST_PATHS,
form.getFieldValue(OpenEdxSebRestriction.ATTR_WHITELIST_PATHS));
additionalAttributes.put(
OpenEdxSebRestriction.ATTR_USER_BANNING_ENABLED,
form.getFieldValue(OpenEdxSebRestriction.ATTR_USER_BANNING_ENABLED));
additionalAttributes.put(
OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS,
Utils.convertCarriageReturnToListSeparator(
form.getFieldValue(OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS)));
}
bodyValue = new SebRestriction(
Long.parseLong(entityKey.modelId),
null,
browserKeys,
additionalAttributes);
} catch (final Exception e) {
e.printStackTrace();
}
return !pageService
.getRestService()
.getBuilder(SaveSebRestriction.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.withBody(bodyValue)
.call()
.onError(formHandle::handleError)
.hasError();
}
private static final class SebRestrictionPropertiesForm
implements ModalInputDialogComposer<FormHandle<?>> {
private final PageService pageService;
private final PageContext pageContext;
protected SebRestrictionPropertiesForm(
final PageService pageService,
final PageContext pageContext) {
this.pageService = pageService;
this.pageContext = pageContext;
}
@Override
public Supplier<FormHandle<?>> compose(final Composite parent) {
final RestService restService = this.pageService.getRestService();
final ResourceService resourceService = this.pageService.getResourceService();
final EntityKey entityKey = this.pageContext.getEntityKey();
final LmsType lmsType = getLmsType(this.pageContext);
final Composite content = this.pageService
.getWidgetFactory()
.createPopupScrollComposite(parent);
final SebRestriction sebRestriction = restService
.getBuilder(GetSebRestriction.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.getOrThrow();
final PageContext formContext = this.pageContext
.copyOf(content)
.clearEntityKeys();
final FormHandle<SebRestriction> formHandle = this.pageService.formBuilder(
formContext)
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(false)
.addField(FormBuilder.text(
"Info",
SEB_RESTRICTION_FORM_INFO,
pageService.getI18nSupport().getText(SEB_RESTRICTION_FORM_INFO_TEXT))
.asArea(50)
.asHTML()
.readonly(true))
.addField(FormBuilder.text(
SebRestriction.ATTR_CONFIG_KEYS,
SEB_RESTRICTION_FORM_CONFIG_KEYS,
StringUtils.join(sebRestriction.getConfigKeys(), Constants.CARRIAGE_RETURN))
.asArea(50)
.readonly(true))
.addField(FormBuilder.text(
SebRestriction.ATTR_BROWSER_KEYS,
SEB_RESTRICTION_FORM_BROWSER_KEYS,
StringUtils.join(sebRestriction.getBrowserExamKeys(), Constants.CARRIAGE_RETURN))
.asArea())
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.multiCheckboxSelection(
OpenEdxSebRestriction.ATTR_WHITELIST_PATHS,
SEB_RESTRICTION_FORM_EDX_WHITE_LIST_PATHS,
sebRestriction.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_WHITELIST_PATHS),
resourceService::sebRestrictionWhiteListResources))
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.multiCheckboxSelection(
OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS,
SEB_RESTRICTION_FORM_EDX_PERMISSIONS,
sebRestriction.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS),
resourceService::sebRestrictionPermissionResources))
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.text(
OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS,
SEB_RESTRICTION_FORM_EDX_BLACKLIST_CHAPTERS,
Utils.convertListSeparatorToCarriageReturn(
sebRestriction
.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS)))
.asArea())
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.checkbox(
OpenEdxSebRestriction.ATTR_USER_BANNING_ENABLED,
SEB_RESTRICTION_FORM_EDX_USER_BANNING_ENABLED,
sebRestriction
.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_USER_BANNING_ENABLED)))
.build();
return () -> formHandle;
}
}
private static LmsType getLmsType(final PageContext pageContext) {
try {
return LmsType.valueOf(pageContext.getAttribute(PAGE_CONTEXT_ATTR_LMS_TYPE));
} catch (final Exception e) {
return null;
}
}
public static PageAction setSebRestriction(
final PageAction action,
final boolean activateRestriction,
final RestService restService) {
return setSebRestriction(
action,
activateRestriction,
restService,
error -> action.pageContext().notifyError(SEB_RESTRICTION_ERROR, error));
}
public static PageAction setSebRestriction(
final PageAction action,
final boolean activateRestriction,
final RestService restService,
final Consumer<Exception> errorHandler) {
restService.getBuilder((activateRestriction)
? ActivateSebRestriction.class
: DeactivateSebRestriction.class)
.withURIVariable(
API.PARAM_MODEL_ID,
action.getEntityKey().modelId)
.call()
.onError(errorHandler);
return action;
}
}

View file

@ -1,275 +1,276 @@
/*
* 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 java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
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.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMapping;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.NewExamConfigMapping;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExamConfigMapping;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNode;
final class ExamToConfigBindingForm {
private static final Logger log = LoggerFactory.getLogger(ExamToConfigBindingForm.class);
private static final LocTextKey NEW_CONFIG_MAPPING_TILE_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.title.new");
private static final LocTextKey CONFIG_MAPPING_TILE_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.title");
private static final LocTextKey CONFIG_MAPPING_NAME_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.name");
private static final LocTextKey FORM_DESCRIPTION_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.description");
private static final LocTextKey FORM_STATUS_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.status");
private static final LocTextKey FORM_ENCRYPT_SECRET_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.encryptSecret");
private static final LocTextKey FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.encryptSecret.confirm");
private final static LocTextKey CONFIG_ACTION_NO_CONFIG_MESSAGE =
new LocTextKey("sebserver.exam.configuration.action.noconfig.message");
static Function<PageAction, PageAction> bindFunction(final PageService pageService) {
return action -> {
final PageContext pageContext = action.pageContext();
final EntityKey entityKey = pageContext.getEntityKey();
final boolean isNew = entityKey == null;
if (isNew) {
final boolean noConfigsAvailable = pageService.getResourceService()
.examConfigurationSelectionResources()
.isEmpty();
if (noConfigsAvailable) {
throw new PageMessageException(CONFIG_ACTION_NO_CONFIG_MESSAGE);
}
}
final ModalInputDialog<FormHandle<ExamConfigurationMap>> dialog =
new ModalInputDialog<FormHandle<ExamConfigurationMap>>(
action.pageContext().getParent().getShell(),
pageService.getWidgetFactory())
.setLargeDialogWidth();
final BindFormContext bindFormContext = new BindFormContext(
pageService,
action.pageContext());
final Predicate<FormHandle<ExamConfigurationMap>> doBind = formHandle -> doCreate(
pageService,
pageContext,
formHandle);
// the default page layout
final LocTextKey titleKey = (isNew)
? NEW_CONFIG_MAPPING_TILE_TEXT_KEY
: CONFIG_MAPPING_TILE_TEXT_KEY;
dialog.open(
titleKey,
doBind,
Utils.EMPTY_EXECUTION,
bindFormContext);
return action;
};
}
private static final boolean doCreate(
final PageService pageService,
final PageContext pageContext,
final FormHandle<ExamConfigurationMap> formHandle) {
final EntityKey entityKey = pageContext.getEntityKey();
final boolean isNew = entityKey == null;
final Class<? extends RestCall<ExamConfigurationMap>> restCall = (isNew)
? NewExamConfigMapping.class
: SaveExamConfigMapping.class;
return !pageService
.getRestService()
.getBuilder(restCall)
.withFormBinding(formHandle.getFormBinding())
.call()
.onError(formHandle::handleError)
.map(mapping -> {
pageService.executePageAction(
pageService.pageActionBuilder(pageContext.clearEntityKeys())
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)
.withEntityKey(pageContext.getParentEntityKey())
.create());
return mapping;
})
.hasError();
}
private static final class BindFormContext implements ModalInputDialogComposer<FormHandle<ExamConfigurationMap>> {
private final PageService pageService;
private final PageContext pageContext;
protected BindFormContext(
final PageService pageService,
final PageContext pageContext) {
this.pageService = pageService;
this.pageContext = pageContext;
}
@Override
public Supplier<FormHandle<ExamConfigurationMap>> compose(final Composite parent) {
final Composite grid = this.pageService.getWidgetFactory()
.createPopupScrollComposite(parent);
final RestService restService = this.pageService.getRestService();
final ResourceService resourceService = this.pageService.getResourceService();
final EntityKey entityKey = this.pageContext.getEntityKey();
final EntityKey parentEntityKey = this.pageContext.getParentEntityKey();
final boolean isNew = entityKey == null;
final Exam exam = (isNew)
? restService
.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.call()
.onError(error -> this.pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow()
: null;
// get data or create new. Handle error if happen
final ExamConfigurationMap examConfigurationMap = (isNew)
? ExamConfigurationMap.createNew(exam)
: restService
.getBuilder(GetExamConfigMapping.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> this.pageContext.notifyLoadError(
EntityType.EXAM_CONFIGURATION_MAP,
error))
.getOrThrow();
// new PageContext with actual EntityKey
final PageContext formContext = this.pageContext.withEntityKey(examConfigurationMap.getEntityKey());
final FormHandle<ExamConfigurationMap> formHandle = this.pageService.formBuilder(
formContext.copyOf(grid))
.readonly(false)
.putStaticValueIf(() -> !isNew,
Domain.EXAM_CONFIGURATION_MAP.ATTR_ID,
examConfigurationMap.getModelId())
.putStaticValue(
Domain.EXAM_CONFIGURATION_MAP.ATTR_INSTITUTION_ID,
String.valueOf(examConfigurationMap.getInstitutionId()))
.putStaticValue(
Domain.EXAM_CONFIGURATION_MAP.ATTR_EXAM_ID,
String.valueOf(examConfigurationMap.examId))
.addField(FormBuilder.singleSelection(
Domain.EXAM_CONFIGURATION_MAP.ATTR_CONFIGURATION_NODE_ID,
CONFIG_MAPPING_NAME_TEXT_KEY,
String.valueOf(examConfigurationMap.configurationNodeId),
resourceService::examConfigurationSelectionResources)
.withSelectionListener(form -> updateFormValuesFromConfigSelection(form, resourceService)))
.addField(FormBuilder.text(
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
FORM_DESCRIPTION_TEXT_KEY,
examConfigurationMap.configDescription)
.asArea()
.readonly(true))
.addField(FormBuilder.text(
Domain.CONFIGURATION_NODE.ATTR_STATUS,
FORM_STATUS_TEXT_KEY,
resourceService.localizedExamConfigStatusName(examConfigurationMap))
.readonly(true))
.addField(FormBuilder.text(
Domain.EXAM_CONFIGURATION_MAP.ATTR_ENCRYPT_SECRET,
FORM_ENCRYPT_SECRET_TEXT_KEY)
.asPasswordField())
.addField(FormBuilder.text(
ExamConfigurationMap.ATTR_CONFIRM_ENCRYPT_SECRET,
FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY)
.asPasswordField())
.build();
return () -> formHandle;
}
}
private static void updateFormValuesFromConfigSelection(final Form form, final ResourceService resourceService) {
final String configId = form.getFieldValue(Domain.EXAM_CONFIGURATION_MAP.ATTR_CONFIGURATION_NODE_ID);
if (StringUtils.isBlank(configId)) {
form.setFieldValue(Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, null);
form.setFieldValue(Domain.CONFIGURATION_NODE.ATTR_STATUS, null);
} else {
try {
final ConfigurationNode configuration = resourceService
.getRestService()
.getBuilder(GetExamConfigNode.class)
.withURIVariable(API.PARAM_MODEL_ID, configId)
.call()
.getOrThrow();
form.setFieldValue(
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
configuration.description);
form.setFieldValue(
Domain.CONFIGURATION_NODE.ATTR_STATUS,
resourceService.localizedExamConfigStatusName(configuration));
} catch (final Exception e) {
log.error("Failed to update form values from SEB Configuration selection", e);
form.setFieldValue(Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, null);
form.setFieldValue(Domain.CONFIGURATION_NODE.ATTR_STATUS, null);
}
}
}
}
/*
* 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 java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
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.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMapping;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.NewExamConfigMapping;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExamConfigMapping;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNode;
final class ExamToConfigBindingForm {
private static final Logger log = LoggerFactory.getLogger(ExamToConfigBindingForm.class);
private static final LocTextKey NEW_CONFIG_MAPPING_TILE_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.title.new");
private static final LocTextKey CONFIG_MAPPING_TILE_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.title");
private static final LocTextKey CONFIG_MAPPING_NAME_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.name");
private static final LocTextKey FORM_DESCRIPTION_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.description");
private static final LocTextKey FORM_STATUS_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.status");
private static final LocTextKey FORM_ENCRYPT_SECRET_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.encryptSecret");
private static final LocTextKey FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY =
new LocTextKey("sebserver.exam.configuration.form.encryptSecret.confirm");
private final static LocTextKey CONFIG_ACTION_NO_CONFIG_MESSAGE =
new LocTextKey("sebserver.exam.configuration.action.noconfig.message");
static Function<PageAction, PageAction> bindFunction(final PageService pageService) {
return action -> {
final PageContext pageContext = action.pageContext();
final EntityKey entityKey = pageContext.getEntityKey();
final boolean isNew = entityKey == null;
if (isNew) {
final boolean noConfigsAvailable = pageService.getResourceService()
.examConfigurationSelectionResources()
.isEmpty();
if (noConfigsAvailable) {
throw new PageMessageException(CONFIG_ACTION_NO_CONFIG_MESSAGE);
}
}
final ModalInputDialog<FormHandle<ExamConfigurationMap>> dialog =
new ModalInputDialog<FormHandle<ExamConfigurationMap>>(
action.pageContext().getParent().getShell(),
pageService.getWidgetFactory())
.setLargeDialogWidth();
final BindFormContext bindFormContext = new BindFormContext(
pageService,
action.pageContext());
final Predicate<FormHandle<ExamConfigurationMap>> doBind = formHandle -> doCreate(
pageService,
pageContext,
formHandle);
// the default page layout
final LocTextKey titleKey = (isNew)
? NEW_CONFIG_MAPPING_TILE_TEXT_KEY
: CONFIG_MAPPING_TILE_TEXT_KEY;
dialog.open(
titleKey,
doBind,
Utils.EMPTY_EXECUTION,
bindFormContext);
return action;
};
}
private static boolean doCreate(
final PageService pageService,
final PageContext pageContext,
final FormHandle<ExamConfigurationMap> formHandle) {
final EntityKey entityKey = pageContext.getEntityKey();
final boolean isNew = entityKey == null;
final Class<? extends RestCall<ExamConfigurationMap>> restCall = (isNew)
? NewExamConfigMapping.class
: SaveExamConfigMapping.class;
return !pageService
.getRestService()
.getBuilder(restCall)
.withFormBinding(formHandle.getFormBinding())
.call()
.onError(formHandle::handleError)
.map(mapping -> {
pageService.executePageAction(
pageService.pageActionBuilder(pageContext.clearEntityKeys())
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)
.withEntityKey(pageContext.getParentEntityKey())
.create());
return mapping;
})
.hasError();
}
private static final class BindFormContext implements ModalInputDialogComposer<FormHandle<ExamConfigurationMap>> {
private final PageService pageService;
private final PageContext pageContext;
protected BindFormContext(
final PageService pageService,
final PageContext pageContext) {
this.pageService = pageService;
this.pageContext = pageContext;
}
@Override
public Supplier<FormHandle<ExamConfigurationMap>> compose(final Composite parent) {
final Composite grid = this.pageService.getWidgetFactory()
.createPopupScrollComposite(parent);
final RestService restService = this.pageService.getRestService();
final ResourceService resourceService = this.pageService.getResourceService();
final EntityKey entityKey = this.pageContext.getEntityKey();
final EntityKey parentEntityKey = this.pageContext.getParentEntityKey();
final boolean isNew = entityKey == null;
final Exam exam = (isNew)
? restService
.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.call()
.onError(error -> this.pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow()
: null;
// get data or create new. Handle error if happen
final ExamConfigurationMap examConfigurationMap = (isNew)
? ExamConfigurationMap.createNew(exam)
: restService
.getBuilder(GetExamConfigMapping.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> this.pageContext.notifyLoadError(
EntityType.EXAM_CONFIGURATION_MAP,
error))
.getOrThrow();
// new PageContext with actual EntityKey
final PageContext formContext = this.pageContext.withEntityKey(examConfigurationMap.getEntityKey());
final FormHandle<ExamConfigurationMap> formHandle = this.pageService.formBuilder(
formContext.copyOf(grid))
.readonly(false)
.putStaticValueIf(() -> !isNew,
Domain.EXAM_CONFIGURATION_MAP.ATTR_ID,
examConfigurationMap.getModelId())
.putStaticValue(
Domain.EXAM_CONFIGURATION_MAP.ATTR_INSTITUTION_ID,
String.valueOf(examConfigurationMap.getInstitutionId()))
.putStaticValue(
Domain.EXAM_CONFIGURATION_MAP.ATTR_EXAM_ID,
String.valueOf(examConfigurationMap.examId))
.addField(FormBuilder.singleSelection(
Domain.EXAM_CONFIGURATION_MAP.ATTR_CONFIGURATION_NODE_ID,
CONFIG_MAPPING_NAME_TEXT_KEY,
String.valueOf(examConfigurationMap.configurationNodeId),
resourceService::examConfigurationSelectionResources)
.withSelectionListener(form -> updateFormValuesFromConfigSelection(form, resourceService))
.mandatory())
.addField(FormBuilder.text(
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
FORM_DESCRIPTION_TEXT_KEY,
examConfigurationMap.configDescription)
.asArea()
.readonly(true))
.addField(FormBuilder.text(
Domain.CONFIGURATION_NODE.ATTR_STATUS,
FORM_STATUS_TEXT_KEY,
resourceService.localizedExamConfigStatusName(examConfigurationMap))
.readonly(true))
.addField(FormBuilder.text(
Domain.EXAM_CONFIGURATION_MAP.ATTR_ENCRYPT_SECRET,
FORM_ENCRYPT_SECRET_TEXT_KEY)
.asPasswordField())
.addField(FormBuilder.text(
ExamConfigurationMap.ATTR_CONFIRM_ENCRYPT_SECRET,
FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY)
.asPasswordField())
.build();
return () -> formHandle;
}
}
private static void updateFormValuesFromConfigSelection(final Form form, final ResourceService resourceService) {
final String configId = form.getFieldValue(Domain.EXAM_CONFIGURATION_MAP.ATTR_CONFIGURATION_NODE_ID);
if (StringUtils.isBlank(configId)) {
form.setFieldValue(Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, null);
form.setFieldValue(Domain.CONFIGURATION_NODE.ATTR_STATUS, null);
} else {
try {
final ConfigurationNode configuration = resourceService
.getRestService()
.getBuilder(GetExamConfigNode.class)
.withURIVariable(API.PARAM_MODEL_ID, configId)
.call()
.getOrThrow();
form.setFieldValue(
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
configuration.description);
form.setFieldValue(
Domain.CONFIGURATION_NODE.ATTR_STATUS,
resourceService.localizedExamConfigStatusName(configuration));
} catch (final Exception e) {
log.error("Failed to update form values from SEB Configuration selection", e);
form.setFieldValue(Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, null);
form.setFieldValue(Domain.CONFIGURATION_NODE.ATTR_STATUS, null);
}
}
}
}

View file

@ -1,203 +1,206 @@
/*
* 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.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.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;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
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.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
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.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicator;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.NewIndicator;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveIndicator;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import io.micrometer.core.instrument.util.StringUtils;
@Lazy
@Component
@GuiProfile
public class IndicatorForm implements TemplateComposer {
private static final LocTextKey NEW_INDICATOR_TILE_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.title.new");
private static final LocTextKey INDICATOR_TILE_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.title");
private static final LocTextKey FORM_THRESHOLDS_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.thresholds");
private static final LocTextKey FORM_COLOR_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.color");
private static final LocTextKey FORM_TYPE_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.type");
private static final LocTextKey FORM_NAME_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.name");
private static final LocTextKey FORM_EXAM_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.exam");
private static final LocTextKey FORM_DESC_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.description");
private static final String INDICATOR_TYPE_DESC_PREFIX =
"sebserver.exam.indicator.type.description.";
private static final String TYPE_DESCRIPTION_FIELD_NAME =
"typeDescription";
private final PageService pageService;
private final ResourceService resourceService;
private final I18nSupport i18nSupport;
protected IndicatorForm(
final PageService pageService,
final ResourceService resourceService) {
this.pageService = pageService;
this.resourceService = resourceService;
this.i18nSupport = pageService.getI18nSupport();
}
@Override
public void compose(final PageContext pageContext) {
final RestService restService = this.resourceService.getRestService();
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
final boolean isNew = entityKey == null;
final boolean isReadonly = pageContext.isReadonly();
final Exam exam = restService
.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.call()
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow();
// get data or create new. Handle error if happen
final Indicator indicator = (isNew)
? Indicator.createNew(exam)
: restService
.getBuilder(GetIndicator.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> pageContext.notifyLoadError(EntityType.INDICATOR, error))
.getOrThrow();
final boolean typeSet = indicator.type != null;
final String typeDescription = (typeSet)
? Utils.formatLineBreaks(this.i18nSupport.getText(INDICATOR_TYPE_DESC_PREFIX + indicator.type.name))
: Constants.EMPTY_NOTE;
// new PageContext with actual EntityKey
final PageContext formContext = pageContext.withEntityKey(indicator.getEntityKey());
// the default page layout
final LocTextKey titleKey = (isNew)
? NEW_INDICATOR_TILE_TEXT_KEY
: INDICATOR_TILE_TEXT_KEY;
final Composite content = widgetFactory.defaultPageLayout(
formContext.getParent(),
titleKey);
final FormHandle<Indicator> formHandle = this.pageService.formBuilder(
formContext.copyOf(content))
.readonly(isReadonly)
.putStaticValueIf(() -> !isNew,
Domain.INDICATOR.ATTR_ID,
indicator.getModelId())
.putStaticValue(
Domain.EXAM.ATTR_INSTITUTION_ID,
String.valueOf(exam.getInstitutionId()))
.putStaticValue(
Domain.INDICATOR.ATTR_EXAM_ID,
parentEntityKey.getModelId())
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
FORM_EXAM_TEXT_KEY,
exam.name)
.readonly(true))
.addField(FormBuilder.text(
Domain.INDICATOR.ATTR_NAME,
FORM_NAME_TEXT_KEY,
indicator.name))
.addField(FormBuilder.singleSelection(
Domain.INDICATOR.ATTR_TYPE,
FORM_TYPE_TEXT_KEY,
(indicator.type != null) ? indicator.type.name() : null,
this.resourceService::indicatorTypeResources)
.withSelectionListener(this::updateForm))
.addField(FormBuilder.text(
TYPE_DESCRIPTION_FIELD_NAME,
FORM_DESC_TEXT_KEY,
typeDescription)
.asArea()
.readonly(true)
.withInputSpan(6))
.addField(FormBuilder.colorSelection(
Domain.INDICATOR.ATTR_COLOR,
FORM_COLOR_TEXT_KEY,
indicator.defaultColor)
.withEmptyCellSeparation(false))
.addField(FormBuilder.thresholdList(
Domain.THRESHOLD.REFERENCE_NAME,
FORM_THRESHOLDS_TEXT_KEY,
indicator))
.buildFor((isNew)
? restService.getRestCall(NewIndicator.class)
: restService.getRestCall(SaveIndicator.class));
// propagate content actions to action-pane
this.pageService.pageActionBuilder(formContext.clearEntityKeys())
.newAction(ActionDefinition.EXAM_INDICATOR_SAVE)
.withEntityKey(parentEntityKey)
.withExec(formHandle::processFormSave)
.ignoreMoveAwayFromEdit()
.publishIf(() -> !isReadonly)
.newAction(ActionDefinition.EXAM_INDICATOR_CANCEL_MODIFY)
.withEntityKey(parentEntityKey)
.withExec(this.pageService.backToCurrentFunction())
.publishIf(() -> !isReadonly);
}
private final void updateForm(final Form form) {
final String typeValue = form.getFieldValue(Domain.INDICATOR.ATTR_TYPE);
if (StringUtils.isNotBlank(typeValue)) {
form.setFieldValue(
TYPE_DESCRIPTION_FIELD_NAME,
this.i18nSupport.getText(INDICATOR_TYPE_DESC_PREFIX + typeValue));
} else {
form.setFieldValue(TYPE_DESCRIPTION_FIELD_NAME, Constants.EMPTY_NOTE);
}
}
}
/*
* 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.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.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;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
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.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
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.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicator;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.NewIndicator;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveIndicator;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import io.micrometer.core.instrument.util.StringUtils;
@Lazy
@Component
@GuiProfile
public class IndicatorForm implements TemplateComposer {
private static final LocTextKey NEW_INDICATOR_TILE_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.title.new");
private static final LocTextKey INDICATOR_TILE_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.title");
private static final LocTextKey FORM_THRESHOLDS_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.thresholds");
private static final LocTextKey FORM_COLOR_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.color");
private static final LocTextKey FORM_TYPE_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.type");
private static final LocTextKey FORM_NAME_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.name");
private static final LocTextKey FORM_EXAM_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.exam");
private static final LocTextKey FORM_DESC_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.form.description");
private static final String INDICATOR_TYPE_DESC_PREFIX =
"sebserver.exam.indicator.type.description.";
private static final String TYPE_DESCRIPTION_FIELD_NAME =
"typeDescription";
private final PageService pageService;
private final ResourceService resourceService;
private final I18nSupport i18nSupport;
protected IndicatorForm(
final PageService pageService,
final ResourceService resourceService) {
this.pageService = pageService;
this.resourceService = resourceService;
this.i18nSupport = pageService.getI18nSupport();
}
@Override
public void compose(final PageContext pageContext) {
final RestService restService = this.resourceService.getRestService();
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
final boolean isNew = entityKey == null;
final boolean isReadonly = pageContext.isReadonly();
final Exam exam = restService
.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.call()
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow();
// get data or create new. Handle error if happen
final Indicator indicator = (isNew)
? Indicator.createNew(exam)
: restService
.getBuilder(GetIndicator.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> pageContext.notifyLoadError(EntityType.INDICATOR, error))
.getOrThrow();
final boolean typeSet = indicator.type != null;
final String typeDescription = (typeSet)
? Utils.formatLineBreaks(this.i18nSupport.getText(INDICATOR_TYPE_DESC_PREFIX + indicator.type.name))
: Constants.EMPTY_NOTE;
// new PageContext with actual EntityKey
final PageContext formContext = pageContext.withEntityKey(indicator.getEntityKey());
// the default page layout
final LocTextKey titleKey = (isNew)
? NEW_INDICATOR_TILE_TEXT_KEY
: INDICATOR_TILE_TEXT_KEY;
final Composite content = widgetFactory.defaultPageLayout(
formContext.getParent(),
titleKey);
final FormHandle<Indicator> formHandle = this.pageService.formBuilder(
formContext.copyOf(content))
.readonly(isReadonly)
.putStaticValueIf(() -> !isNew,
Domain.INDICATOR.ATTR_ID,
indicator.getModelId())
.putStaticValue(
Domain.EXAM.ATTR_INSTITUTION_ID,
String.valueOf(exam.getInstitutionId()))
.putStaticValue(
Domain.INDICATOR.ATTR_EXAM_ID,
parentEntityKey.getModelId())
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
FORM_EXAM_TEXT_KEY,
exam.name)
.readonly(true))
.addField(FormBuilder.text(
Domain.INDICATOR.ATTR_NAME,
FORM_NAME_TEXT_KEY,
indicator.name)
.mandatory(!isReadonly))
.addField(FormBuilder.singleSelection(
Domain.INDICATOR.ATTR_TYPE,
FORM_TYPE_TEXT_KEY,
(indicator.type != null) ? indicator.type.name() : null,
this.resourceService::indicatorTypeResources)
.withSelectionListener(this::updateForm)
.mandatory(!isReadonly))
.addField(FormBuilder.text(
TYPE_DESCRIPTION_FIELD_NAME,
FORM_DESC_TEXT_KEY,
typeDescription)
.asArea()
.asHTML(true)
.readonly(true)
.withInputSpan(6))
.addField(FormBuilder.colorSelection(
Domain.INDICATOR.ATTR_COLOR,
FORM_COLOR_TEXT_KEY,
indicator.defaultColor)
.withEmptyCellSeparation(false))
.addField(FormBuilder.thresholdList(
Domain.THRESHOLD.REFERENCE_NAME,
FORM_THRESHOLDS_TEXT_KEY,
indicator))
.buildFor((isNew)
? restService.getRestCall(NewIndicator.class)
: restService.getRestCall(SaveIndicator.class));
// propagate content actions to action-pane
this.pageService.pageActionBuilder(formContext.clearEntityKeys())
.newAction(ActionDefinition.EXAM_INDICATOR_SAVE)
.withEntityKey(parentEntityKey)
.withExec(formHandle::processFormSave)
.ignoreMoveAwayFromEdit()
.publishIf(() -> !isReadonly)
.newAction(ActionDefinition.EXAM_INDICATOR_CANCEL_MODIFY)
.withEntityKey(parentEntityKey)
.withExec(this.pageService.backToCurrentFunction())
.publishIf(() -> !isReadonly);
}
private void updateForm(final Form form) {
final String typeValue = form.getFieldValue(Domain.INDICATOR.ATTR_TYPE);
if (StringUtils.isNotBlank(typeValue)) {
form.setFieldValue(
TYPE_DESCRIPTION_FIELD_NAME,
this.i18nSupport.getText(INDICATOR_TYPE_DESC_PREFIX + typeValue));
} else {
form.setFieldValue(TYPE_DESCRIPTION_FIELD_NAME, Constants.EMPTY_NOTE);
}
}
}

View file

@ -1,420 +1,428 @@
/*
* 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 java.util.function.BooleanSupplier;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite;
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.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
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;
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.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
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.PageContext.AttributeKeys;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitution;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.ActivateLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.DeactivateLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.NewLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.SaveLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetupAdHoc;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.EntityGrantCheck;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Lazy
@Component
@GuiProfile
public class LmsSetupForm implements TemplateComposer {
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.title");
private static final LocTextKey NEW_TITLE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.title.new");
private static final LocTextKey FORM_SECRET_LMS_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.secret.lms");
private static final LocTextKey FORM_CLIENTNAME_LMS_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.clientname.lms");
private static final LocTextKey FORM_URL_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.url");
private static final LocTextKey FORM_TYPE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.type");
private static final LocTextKey FORM_NAME_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.name");
private static final LocTextKey FORM_INSTITUTION_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.institution");
private static final LocTextKey FORM_PROXY_KEY =
new LocTextKey("sebserver.lmssetup.form.proxy");
private static final LocTextKey FORM_PROXY_HOST_KEY =
new LocTextKey("sebserver.lmssetup.form.proxy.host");
private static final LocTextKey FORM_PROXY_PORT_KEY =
new LocTextKey("sebserver.lmssetup.form.proxy.port");
private static final LocTextKey FORM_PROXY_AUTH_CREDENTIALS_KEY =
new LocTextKey("sebserver.lmssetup.form.proxy.auth-credentials");
private final PageService pageService;
private final ResourceService resourceService;
protected LmsSetupForm(
final PageService pageService,
final ResourceService resourceService) {
this.pageService = pageService;
this.resourceService = resourceService;
}
@Override
public void compose(final PageContext pageContext) {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final UserInfo user = currentUser.get();
final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
final boolean readonly = pageContext.isReadonly();
final BooleanSupplier isNew = () -> entityKey == null;
final BooleanSupplier isNotNew = () -> !isNew.getAsBoolean();
final BooleanSupplier isSEBAdmin = () -> user.hasRole(UserRole.SEB_SERVER_ADMIN);
final BooleanSupplier isEdit = () -> !readonly;
// get data or create new. handle error if happen
final LmsSetup lmsSetup = isNew.getAsBoolean()
? LmsSetup.createNew((parentEntityKey != null)
? Long.valueOf(parentEntityKey.modelId)
: user.institutionId)
: restService
.getBuilder(GetLmsSetup.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> pageContext.notifyLoadError(EntityType.LMS_SETUP, error))
.getOrThrow();
// new PageContext with actual EntityKey
final PageContext formContext = pageContext.withEntityKey(lmsSetup.getEntityKey());
// the default page layout with title
final LocTextKey titleKey = isNotNew.getAsBoolean()
? TITLE_TEXT_KEY
: NEW_TITLE_TEXT_KEY;
final Composite content = widgetFactory.defaultPageLayout(
formContext.getParent(),
titleKey);
final EntityGrantCheck userGrantCheck = currentUser.entityGrantCheck(lmsSetup);
final boolean writeGrant = userGrantCheck.w();
final boolean modifyGrant = userGrantCheck.m();
final boolean institutionActive = restService.getBuilder(GetInstitution.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(lmsSetup.getInstitutionId()))
.call()
.map(inst -> inst.active)
.getOr(false);
// The LMS Setup form
final LmsType lmsType = lmsSetup.getLmsType();
final FormHandle<LmsSetup> formHandle = this.pageService.formBuilder(
formContext.copyOf(content), 8)
.withDefaultSpanLabel(2)
.withDefaultSpanInput(5)
.withDefaultSpanEmptyCell(1)
.readonly(readonly)
.putStaticValueIf(isNotNew,
Domain.LMS_SETUP.ATTR_ID,
lmsSetup.getModelId())
.putStaticValue(
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
String.valueOf(lmsSetup.getInstitutionId()))
.putStaticValueIf(isNotNew,
Domain.LMS_SETUP.ATTR_LMS_TYPE,
String.valueOf(lmsSetup.getLmsType()))
.addFieldIf(
isSEBAdmin,
() -> FormBuilder.singleSelection(
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
FORM_INSTITUTION_TEXT_KEY,
String.valueOf(lmsSetup.getInstitutionId()),
() -> this.resourceService.institutionResource())
.readonly(true))
.addField(FormBuilder.text(
Domain.LMS_SETUP.ATTR_NAME,
FORM_NAME_TEXT_KEY,
lmsSetup.getName()))
.addField(FormBuilder.singleSelection(
Domain.LMS_SETUP.ATTR_LMS_TYPE,
FORM_TYPE_TEXT_KEY,
(lmsType != null) ? lmsType.name() : LmsType.MOCKUP.name(),
this.resourceService::lmsTypeResources)
.readonlyIf(isNotNew))
.addField(FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_URL,
FORM_URL_TEXT_KEY,
lmsSetup.getLmsApiUrl()))
.addField(FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_CLIENTNAME,
FORM_CLIENTNAME_LMS_TEXT_KEY,
(lmsSetup.getLmsAuthName() != null) ? lmsSetup.getLmsAuthName() : null))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_CLIENTSECRET,
FORM_SECRET_LMS_TEXT_KEY)
.asPasswordField())
.addFieldIf(
() -> readonly,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_PROXY_HOST,
FORM_PROXY_KEY,
(StringUtils.isNotBlank(lmsSetup.getProxyHost()))
? lmsSetup.getProxyHost() + Constants.URL_PORT_SEPARATOR + lmsSetup.proxyPort
: null))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_PROXY_HOST,
FORM_PROXY_HOST_KEY,
(StringUtils.isNotBlank(lmsSetup.getProxyHost())) ? lmsSetup.getProxyHost() : null)
.withInputSpan(3)
.withEmptyCellSpan(0))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_PROXY_PORT,
FORM_PROXY_PORT_KEY,
(lmsSetup.getProxyPort() != null) ? String.valueOf(lmsSetup.getProxyPort()) : null)
.asNumber(number -> {
if (StringUtils.isNotBlank(number)) {
Integer.parseInt(number);
}
})
.withInputSpan(1)
.withLabelSpan(1)
.withEmptyCellSeparation(false)
.withEmptyCellSpan(0))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_PROXY_AUTH_USERNAME,
FORM_PROXY_AUTH_CREDENTIALS_KEY,
(lmsSetup.getProxyAuthUsername() != null) ? lmsSetup.getProxyAuthUsername() : null)
.withInputSpan(3)
.withEmptyCellSpan(0))
.addFieldIf(
isEdit,
() -> FormBuilder.text(Domain.LMS_SETUP.ATTR_LMS_PROXY_AUTH_SECRET)
.asPasswordField()
.withInputSpan(2)
.withLabelSpan(0)
.withEmptyCellSeparation(false)
.withEmptyCellSpan(0))
.buildFor((entityKey == null)
? restService.getRestCall(NewLmsSetup.class)
: restService.getRestCall(SaveLmsSetup.class));
// propagate content actions to action-pane
this.pageService.pageActionBuilder(formContext.clearEntityKeys())
.newAction(ActionDefinition.LMS_SETUP_NEW)
.publishIf(() -> writeGrant && readonly && institutionActive)
.newAction(ActionDefinition.LMS_SETUP_MODIFY)
.withEntityKey(entityKey)
.publishIf(() -> modifyGrant && readonly && institutionActive)
.newAction(ActionDefinition.LMS_SETUP_TEST_AND_SAVE)
.withEntityKey(entityKey)
.withExec(action -> this.testAdHoc(action, formHandle))
.ignoreMoveAwayFromEdit()
.publishIf(() -> modifyGrant && !readonly)
.newAction(ActionDefinition.LMS_SETUP_DEACTIVATE)
.withEntityKey(entityKey)
.withSimpleRestCall(restService, DeactivateLmsSetup.class)
.withConfirm(this.pageService.confirmDeactivation(lmsSetup))
.publishIf(() -> writeGrant && readonly && institutionActive && lmsSetup.isActive())
.newAction(ActionDefinition.LMS_SETUP_ACTIVATE)
.withEntityKey(entityKey)
.withExec(action -> activate(action, formHandle))
.publishIf(() -> writeGrant && readonly && institutionActive && !lmsSetup.isActive())
.newAction(ActionDefinition.LMS_SETUP_SAVE)
.withEntityKey(entityKey)
.withExec(formHandle::processFormSave)
.ignoreMoveAwayFromEdit()
.publishIf(() -> !readonly)
.newAction(ActionDefinition.LMS_SETUP_CANCEL_MODIFY)
.withEntityKey(entityKey)
.withExec(this.pageService.backToCurrentFunction())
.publishIf(() -> !readonly);
}
/** Save and test connection before activation */
private PageAction activate(final PageAction action, final FormHandle<LmsSetup> formHandle) {
// first test the LMS Setup. If this fails the action execution will stops
final PageAction testLmsSetup = this.testLmsSetup(action, formHandle, false);
// if LMS Setup test was successful, the activation action applies
this.resourceService.getRestService().getBuilder(ActivateLmsSetup.class)
.withURIVariable(
API.PARAM_MODEL_ID,
action.pageContext().getAttribute(AttributeKeys.ENTITY_ID))
.call()
.onError(error -> action.pageContext().notifyActivationError(EntityType.LMS_SETUP, error));
return testLmsSetup;
}
/** LmsSetup test action implementation */
private PageAction testAdHoc(final PageAction action, final FormHandle<LmsSetup> formHandle) {
// reset previous errors
formHandle.process(
Utils.truePredicate(),
fieldAccessor -> fieldAccessor.resetError());
// first test the connection on ad hoc object
final Result<LmsSetupTestResult> result = this.resourceService.getRestService()
.getBuilder(TestLmsSetupAdHoc.class)
.withFormBinding(formHandle.getFormBinding())
.call();
// ... and handle the response
if (result.hasError()) {
if (formHandle.handleError(result.getError())) {
final Exception error = result.getError();
if (error instanceof RestCallError) {
throw (RestCallError) error;
} else {
throw new RuntimeException("Cause: ", error);
}
}
}
return handleTestResult(
action,
a -> {
// try to save the LmsSetup
final PageAction processFormSave = formHandle.processFormSave(a);
processFormSave.pageContext().publishInfo(
new LocTextKey("sebserver.lmssetup.action.test.ok"));
return processFormSave;
},
result.getOrThrow());
}
/** LmsSetup test action implementation */
private PageAction testLmsSetup(
final PageAction action,
final FormHandle<LmsSetup> formHandle, final boolean saveFirst) {
if (saveFirst) {
final Result<LmsSetup> postResult = formHandle.doAPIPost();
if (postResult.hasError()) {
formHandle.handleError(postResult.getError());
postResult.getOrThrow();
}
}
// Call the testing endpoint with the specified data to test
final EntityKey entityKey = action.getEntityKey();
final RestService restService = this.resourceService.getRestService();
final Result<LmsSetupTestResult> result = restService.getBuilder(TestLmsSetup.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.getModelId())
.call();
// ... and handle the response
if (result.hasError()) {
if (formHandle.handleError(result.getError())) {
throw new PageMessageException(
new LocTextKey("sebserver.lmssetup.action.test.missingParameter"));
}
}
return handleTestResult(
action,
a -> {
action.pageContext().publishInfo(
new LocTextKey("sebserver.lmssetup.action.test.ok"));
return action;
},
result.getOrThrow());
}
private PageAction handleTestResult(
final PageAction action,
final Function<PageAction, PageAction> onOK,
final LmsSetupTestResult testResult) {
if (testResult.isOk()) {
return onOK.apply(action);
}
testResult.errors
.stream()
.findFirst()
.ifPresent(error -> {
switch (error.errorType) {
case TOKEN_REQUEST: {
throw new PageMessageException(new LocTextKey(
"sebserver.lmssetup.action.test.tokenRequestError",
error.message));
}
case QUIZ_ACCESS_API_REQUEST: {
throw new PageMessageException(new LocTextKey(
"sebserver.lmssetup.action.test.quizRequestError",
error.message));
}
case QUIZ_RESTRICTION_API_REQUEST: {
// NOTE: quiz restriction is not mandatory for functional LmsSetup
// so this error is ignored here
break;
}
default: {
throw new PageMessageException(new LocTextKey(
"sebserver.lmssetup.action.test.unknownError",
error.message));
}
}
});
return onOK.apply(action);
}
}
/*
* 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 java.util.function.BooleanSupplier;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gui.form.Form;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite;
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.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
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;
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.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
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.PageContext.AttributeKeys;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitution;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.ActivateLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.DeactivateLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.NewLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.SaveLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetupAdHoc;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.EntityGrantCheck;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Lazy
@Component
@GuiProfile
public class LmsSetupForm implements TemplateComposer {
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.title");
private static final LocTextKey NEW_TITLE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.title.new");
private static final LocTextKey FORM_SECRET_LMS_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.secret.lms");
private static final LocTextKey FORM_CLIENTNAME_LMS_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.clientname.lms");
private static final LocTextKey FORM_URL_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.url");
private static final LocTextKey FORM_TYPE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.type");
private static final LocTextKey FORM_NAME_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.name");
private static final LocTextKey FORM_INSTITUTION_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.form.institution");
private static final LocTextKey FORM_PROXY_KEY =
new LocTextKey("sebserver.lmssetup.form.proxy");
private static final LocTextKey FORM_PROXY_HOST_KEY =
new LocTextKey("sebserver.lmssetup.form.proxy.host");
private static final LocTextKey FORM_PROXY_PORT_KEY =
new LocTextKey("sebserver.lmssetup.form.proxy.port");
private static final LocTextKey FORM_PROXY_AUTH_CREDENTIALS_KEY =
new LocTextKey("sebserver.lmssetup.form.proxy.auth-credentials");
public static final LocTextKey LMS_SETUP_TEST_OK =
new LocTextKey("sebserver.lmssetup.action.test.ok");
private final PageService pageService;
private final ResourceService resourceService;
protected LmsSetupForm(
final PageService pageService,
final ResourceService resourceService) {
this.pageService = pageService;
this.resourceService = resourceService;
}
@Override
public void compose(final PageContext pageContext) {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final UserInfo user = currentUser.get();
final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
final boolean readonly = pageContext.isReadonly();
final BooleanSupplier isNew = () -> entityKey == null;
final BooleanSupplier isNotNew = () -> !isNew.getAsBoolean();
final BooleanSupplier isSEBAdmin = () -> user.hasRole(UserRole.SEB_SERVER_ADMIN);
final BooleanSupplier isEdit = () -> !readonly;
// get data or create new. handle error if happen
final LmsSetup lmsSetup = isNew.getAsBoolean()
? LmsSetup.createNew((parentEntityKey != null)
? Long.valueOf(parentEntityKey.modelId)
: user.institutionId)
: restService
.getBuilder(GetLmsSetup.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> pageContext.notifyLoadError(EntityType.LMS_SETUP, error))
.getOrThrow();
// new PageContext with actual EntityKey
final PageContext formContext = pageContext.withEntityKey(lmsSetup.getEntityKey());
// the default page layout with title
final LocTextKey titleKey = isNotNew.getAsBoolean()
? TITLE_TEXT_KEY
: NEW_TITLE_TEXT_KEY;
final Composite content = widgetFactory.defaultPageLayout(
formContext.getParent(),
titleKey);
final EntityGrantCheck userGrantCheck = currentUser.entityGrantCheck(lmsSetup);
final boolean writeGrant = userGrantCheck.w();
final boolean modifyGrant = userGrantCheck.m();
final boolean institutionActive = restService.getBuilder(GetInstitution.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(lmsSetup.getInstitutionId()))
.call()
.map(inst -> inst.active)
.getOr(false);
// The LMS Setup form
final LmsType lmsType = lmsSetup.getLmsType();
final FormHandle<LmsSetup> formHandle = this.pageService.formBuilder(
formContext.copyOf(content), 8)
.withDefaultSpanLabel(2)
.withDefaultSpanInput(5)
.withDefaultSpanEmptyCell(1)
.readonly(readonly)
.putStaticValueIf(isNotNew,
Domain.LMS_SETUP.ATTR_ID,
lmsSetup.getModelId())
.putStaticValue(
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
String.valueOf(lmsSetup.getInstitutionId()))
.putStaticValueIf(isNotNew,
Domain.LMS_SETUP.ATTR_LMS_TYPE,
String.valueOf(lmsSetup.getLmsType()))
.addFieldIf(
isSEBAdmin,
() -> FormBuilder.singleSelection(
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
FORM_INSTITUTION_TEXT_KEY,
String.valueOf(lmsSetup.getInstitutionId()),
this.resourceService::institutionResource)
.readonly(true))
.addField(FormBuilder.text(
Domain.LMS_SETUP.ATTR_NAME,
FORM_NAME_TEXT_KEY,
lmsSetup.getName())
.mandatory(!readonly))
.addField(FormBuilder.singleSelection(
Domain.LMS_SETUP.ATTR_LMS_TYPE,
FORM_TYPE_TEXT_KEY,
(lmsType != null) ? lmsType.name() : LmsType.MOCKUP.name(),
this.resourceService::lmsTypeResources)
.readonlyIf(isNotNew)
.mandatory(!readonly))
.addField(FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_URL,
FORM_URL_TEXT_KEY,
lmsSetup.getLmsApiUrl())
.mandatory(!readonly))
.addField(FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_CLIENTNAME,
FORM_CLIENTNAME_LMS_TEXT_KEY,
lmsSetup.getLmsAuthName())
.mandatory(!readonly))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_CLIENTSECRET,
FORM_SECRET_LMS_TEXT_KEY)
.asPasswordField()
.mandatory(!readonly))
.addFieldIf(
() -> readonly,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_PROXY_HOST,
FORM_PROXY_KEY,
(StringUtils.isNotBlank(lmsSetup.getProxyHost()))
? lmsSetup.getProxyHost() + Constants.URL_PORT_SEPARATOR + lmsSetup.proxyPort
: null))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_PROXY_HOST,
FORM_PROXY_HOST_KEY,
(StringUtils.isNotBlank(lmsSetup.getProxyHost())) ? lmsSetup.getProxyHost() : null)
.withInputSpan(3)
.withEmptyCellSpan(0))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_PROXY_PORT,
FORM_PROXY_PORT_KEY,
(lmsSetup.getProxyPort() != null) ? String.valueOf(lmsSetup.getProxyPort()) : null)
.asNumber(number -> {
if (StringUtils.isNotBlank(number)) {
Integer.parseInt(number);
}
})
.withInputSpan(1)
.withLabelSpan(1)
.withEmptyCellSeparation(false)
.withEmptyCellSpan(0))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_PROXY_AUTH_USERNAME,
FORM_PROXY_AUTH_CREDENTIALS_KEY,
lmsSetup.getProxyAuthUsername())
.withInputSpan(3)
.withEmptyCellSpan(0))
.addFieldIf(
isEdit,
() -> FormBuilder.text(Domain.LMS_SETUP.ATTR_LMS_PROXY_AUTH_SECRET)
.asPasswordField()
.withInputSpan(2)
.withLabelSpan(0)
.withEmptyCellSeparation(false)
.withEmptyCellSpan(0))
.buildFor((entityKey == null)
? restService.getRestCall(NewLmsSetup.class)
: restService.getRestCall(SaveLmsSetup.class));
// propagate content actions to action-pane
this.pageService.pageActionBuilder(formContext.clearEntityKeys())
.newAction(ActionDefinition.LMS_SETUP_NEW)
.publishIf(() -> writeGrant && readonly && institutionActive)
.newAction(ActionDefinition.LMS_SETUP_MODIFY)
.withEntityKey(entityKey)
.publishIf(() -> modifyGrant && readonly && institutionActive)
.newAction(ActionDefinition.LMS_SETUP_DEACTIVATE)
.withEntityKey(entityKey)
.withSimpleRestCall(restService, DeactivateLmsSetup.class)
.withConfirm(this.pageService.confirmDeactivation(lmsSetup))
.publishIf(() -> writeGrant && readonly && institutionActive && lmsSetup.isActive())
.newAction(ActionDefinition.LMS_SETUP_ACTIVATE)
.withEntityKey(entityKey)
.withExec(action -> activate(action, formHandle, restService))
.publishIf(() -> writeGrant && readonly && institutionActive && !lmsSetup.isActive())
.newAction(ActionDefinition.LMS_SETUP_SAVE)
.withEntityKey(entityKey)
.withExec(formHandle::processFormSave)
.ignoreMoveAwayFromEdit()
.publishIf(() -> !readonly)
.newAction(ActionDefinition.LMS_SETUP_SAVE_AND_ACTIVATE)
.withEntityKey(entityKey)
.withExec(action -> {
this.testAdHoc(action, formHandle);
PageAction newAction = formHandle.saveAndActivate(action);
pageContext.publishInfo(LMS_SETUP_TEST_OK);
return newAction;
})
.ignoreMoveAwayFromEdit()
.publishIf(() -> !readonly && !lmsSetup.isActive())
.newAction(ActionDefinition.LMS_SETUP_CANCEL_MODIFY)
.withEntityKey(entityKey)
.withExec(this.pageService.backToCurrentFunction())
.publishIf(() -> !readonly);
}
/** Save and test connection before activation */
public static PageAction activate(
final PageAction action,
final FormHandle<LmsSetup> formHandle,
final RestService restService) {
// first test the LMS Setup. If this fails the action execution will stops
final PageAction testLmsSetup = testLmsSetup(action, formHandle, restService);
// if LMS Setup test was successful, the activation action applies
restService.getBuilder(ActivateLmsSetup.class)
.withURIVariable(
API.PARAM_MODEL_ID,
action.pageContext().getAttribute(AttributeKeys.ENTITY_ID))
.call()
.onError(error -> action.pageContext().notifyActivationError(EntityType.LMS_SETUP, error));
return testLmsSetup;
}
/** LmsSetup test action implementation */
private PageAction testAdHoc(final PageAction action, final FormHandle<LmsSetup> formHandle) {
// reset previous errors
formHandle.process(
Utils.truePredicate(),
Form.FormFieldAccessor::resetError);
// first test the connection on ad hoc object
final Result<LmsSetupTestResult> result = this.resourceService.getRestService()
.getBuilder(TestLmsSetupAdHoc.class)
.withFormBinding(formHandle.getFormBinding())
.call();
// ... and handle the response
if (result.hasError()) {
if (formHandle.handleError(result.getError())) {
final Exception error = result.getError();
if (error instanceof RestCallError) {
throw (RestCallError) error;
} else {
throw new RuntimeException("Cause: ", error);
}
}
}
return action;
}
/** LmsSetup test action implementation */
public static PageAction testLmsSetup(
final PageAction action,
final FormHandle<LmsSetup> formHandle,
final RestService restService) {
// Call the testing endpoint with the specified data to test
final EntityKey entityKey = action.getEntityKey();
final Result<LmsSetupTestResult> result = restService.getBuilder(TestLmsSetup.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.getModelId())
.call();
// ... and handle the response
if (result.hasError()) {
if (formHandle != null && formHandle.handleError(result.getError())) {
throw new PageMessageException(
new LocTextKey("sebserver.lmssetup.action.test.missingParameter"));
}
result.getOrThrow();
}
return handleTestResult(
action,
a -> {
action.pageContext().publishInfo(
new LocTextKey("sebserver.lmssetup.action.test.ok"));
return action;
},
result.getOrThrow());
}
private static PageAction handleTestResult(
final PageAction action,
final Function<PageAction, PageAction> onOK,
final LmsSetupTestResult testResult) {
if (testResult.isOk()) {
return onOK.apply(action);
}
testResult.errors
.stream()
.findFirst()
.ifPresent(error -> {
switch (error.errorType) {
case TOKEN_REQUEST: {
throw new PageMessageException(new LocTextKey(
"sebserver.lmssetup.action.test.tokenRequestError",
error.message));
}
case QUIZ_ACCESS_API_REQUEST: {
throw new PageMessageException(new LocTextKey(
"sebserver.lmssetup.action.test.quizRequestError",
error.message));
}
case QUIZ_RESTRICTION_API_REQUEST: {
// NOTE: quiz restriction is not mandatory for functional LmsSetup
// so this error is ignored here
break;
}
default: {
throw new PageMessageException(new LocTextKey(
"sebserver.lmssetup.action.test.unknownError",
error.message));
}
}
});
return onOK.apply(action);
}
}

View file

@ -1,185 +1,204 @@
/*
* 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 java.util.function.Function;
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.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
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.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.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck;
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 LmsSetupList implements TemplateComposer {
private static final LocTextKey NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION =
new LocTextKey("sebserver.lmssetup.list.action.no.modify.privilege");
private static final LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.info.pleaseSelect");
private static final LocTextKey ACTIVITY_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.column.active");
private static final LocTextKey TYPE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.column.type");
private static final LocTextKey NAME_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.column.name");
private static final LocTextKey INSTITUTION_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.column.institution");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.empty");
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.title");
private final TableFilterAttribute institutionFilter;
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, Entity.FILTER_ATTR_NAME);
private final TableFilterAttribute typeFilter;
private final TableFilterAttribute activityFilter;
private final PageService pageService;
private final ResourceService resourceService;
private final int pageSize;
protected LmsSetupList(
final PageService pageService,
final ResourceService resourceService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.resourceService = resourceService;
this.pageSize = pageSize;
this.institutionFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Entity.FILTER_ATTR_INSTITUTION,
this.resourceService::institutionResource);
this.typeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
LmsSetup.FILTER_ATTR_LMS_TYPE,
this.resourceService::lmsTypeResources);
this.activityFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
UserInfo.FILTER_ATTR_ACTIVE,
this.resourceService::activityResources);
}
@Override
public void compose(final PageContext pageContext) {
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(),
TITLE_TEXT_KEY);
final boolean isSEBAdmin = currentUser.get().hasRole(UserRole.SEB_SERVER_ADMIN);
final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(pageContext.clearEntityKeys());
// table
final EntityTable<LmsSetup> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetLmsSetupPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withColumnIf(
() -> isSEBAdmin,
() -> new ColumnDefinition<>(
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
INSTITUTION_TEXT_KEY,
lmsSetupInstitutionNameFunction(this.resourceService))
.withFilter(this.institutionFilter))
.withColumn(new ColumnDefinition<>(
Domain.LMS_SETUP.ATTR_NAME,
NAME_TEXT_KEY,
LmsSetup::getName)
.withFilter(this.nameFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
Domain.LMS_SETUP.ATTR_LMS_TYPE,
TYPE_TEXT_KEY,
this::lmsSetupTypeName)
.withFilter(this.typeFilter)
.localized()
.sortable())
.withColumn(new ColumnDefinition<>(
Domain.LMS_SETUP.ATTR_ACTIVE,
ACTIVITY_TEXT_KEY,
this.pageService.getResourceService().<LmsSetup> localizedActivityFunction())
.withFilter(this.activityFilter)
.sortable())
.withDefaultAction(actionBuilder
.newAction(ActionDefinition.LMS_SETUP_VIEW_FROM_LIST)
.create())
.compose(pageContext.copyOf(content));
// propagate content actions to action-pane
final GrantCheck userGrant = currentUser.grantCheck(EntityType.LMS_SETUP);
actionBuilder
.newAction(ActionDefinition.LMS_SETUP_NEW)
.publishIf(userGrant::iw)
.newAction(ActionDefinition.LMS_SETUP_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> table.hasAnyContent())
.newAction(ActionDefinition.LMS_SETUP_MODIFY_FROM_LIST)
.withSelect(
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION),
PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im() && table.hasAnyContent());
}
private String lmsSetupTypeName(final LmsSetup lmsSetup) {
if (lmsSetup.lmsType == null) {
return Constants.EMPTY_NOTE;
}
return this.resourceService.getI18nSupport()
.getText(ResourceService.LMSSETUP_TYPE_PREFIX + lmsSetup.lmsType.name());
}
private static Function<LmsSetup, String> lmsSetupInstitutionNameFunction(final ResourceService resourceService) {
return lmsSetup -> resourceService.getInstitutionNameFunction()
.apply(String.valueOf(lmsSetup.institutionId));
}
}
/*
* 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 java.util.function.Function;
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.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
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.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.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck;
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 LmsSetupList implements TemplateComposer {
private static final LocTextKey NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION =
new LocTextKey("sebserver.lmssetup.list.action.no.modify.privilege");
private static final LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.info.pleaseSelect");
private static final LocTextKey ACTIVITY_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.column.active");
private static final LocTextKey TYPE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.column.type");
private static final LocTextKey NAME_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.column.name");
private static final LocTextKey INSTITUTION_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.column.institution");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.empty");
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.lmssetup.list.title");
private final TableFilterAttribute institutionFilter;
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, Entity.FILTER_ATTR_NAME);
private final TableFilterAttribute typeFilter;
private final TableFilterAttribute activityFilter;
private final PageService pageService;
private final ResourceService resourceService;
private final int pageSize;
protected LmsSetupList(
final PageService pageService,
final ResourceService resourceService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.resourceService = resourceService;
this.pageSize = pageSize;
this.institutionFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Entity.FILTER_ATTR_INSTITUTION,
this.resourceService::institutionResource);
this.typeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
LmsSetup.FILTER_ATTR_LMS_TYPE,
this.resourceService::lmsTypeResources);
this.activityFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
UserInfo.FILTER_ATTR_ACTIVE,
this.resourceService::activityResources);
}
@Override
public void compose(final PageContext pageContext) {
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(),
TITLE_TEXT_KEY);
final boolean isSEBAdmin = currentUser.get().hasRole(UserRole.SEB_SERVER_ADMIN);
final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(pageContext.clearEntityKeys());
// table
final EntityTable<LmsSetup> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetLmsSetupPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withColumnIf(
() -> isSEBAdmin,
() -> new ColumnDefinition<>(
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
INSTITUTION_TEXT_KEY,
lmsSetupInstitutionNameFunction(this.resourceService))
.withFilter(this.institutionFilter))
.withColumn(new ColumnDefinition<>(
Domain.LMS_SETUP.ATTR_NAME,
NAME_TEXT_KEY,
LmsSetup::getName)
.withFilter(this.nameFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
Domain.LMS_SETUP.ATTR_LMS_TYPE,
TYPE_TEXT_KEY,
this::lmsSetupTypeName)
.withFilter(this.typeFilter)
.localized()
.sortable())
.withColumn(new ColumnDefinition<>(
Domain.LMS_SETUP.ATTR_ACTIVE,
ACTIVITY_TEXT_KEY,
this.pageService.getResourceService().<LmsSetup> localizedActivityFunction())
.withFilter(this.activityFilter)
.sortable())
.withDefaultAction(actionBuilder
.newAction(ActionDefinition.LMS_SETUP_VIEW_FROM_LIST)
.create())
.withSelectionListener(this.pageService.getSelectionPublisher(
ActionDefinition.LMS_SETUP_TOGGLE_ACTIVITY,
ActionDefinition.LMS_SETUP_ACTIVATE,
ActionDefinition.LMS_SETUP_DEACTIVATE,
pageContext,
ActionDefinition.LMS_SETUP_VIEW_FROM_LIST,
ActionDefinition.LMS_SETUP_MODIFY_FROM_LIST,
ActionDefinition.LMS_SETUP_TOGGLE_ACTIVITY))
.compose(pageContext.copyOf(content));
// propagate content actions to action-pane
final GrantCheck userGrant = currentUser.grantCheck(EntityType.LMS_SETUP);
actionBuilder
.newAction(ActionDefinition.LMS_SETUP_NEW)
.publishIf(userGrant::iw)
.newAction(ActionDefinition.LMS_SETUP_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.publishIf(table::hasAnyContent, false)
.newAction(ActionDefinition.LMS_SETUP_MODIFY_FROM_LIST)
.withSelect(
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION),
PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im() && table.hasAnyContent(), false)
.newAction(ActionDefinition.LMS_SETUP_TOGGLE_ACTIVITY)
.withExec(this.pageService.activationToggleActionFunction(
table,
EMPTY_SELECTION_TEXT_KEY,
action -> LmsSetupForm.testLmsSetup(action, null, restService)))
.withConfirm(this.pageService.confirmDeactivation(table))
.publishIf(() -> userGrant.im() && table.hasAnyContent(), false);
}
private String lmsSetupTypeName(final LmsSetup lmsSetup) {
if (lmsSetup.lmsType == null) {
return Constants.EMPTY_NOTE;
}
return this.resourceService.getI18nSupport()
.getText(ResourceService.LMSSETUP_TYPE_PREFIX + lmsSetup.lmsType.name());
}
private static Function<LmsSetup, String> lmsSetupInstitutionNameFunction(final ResourceService resourceService) {
return lmsSetup -> resourceService.getInstitutionNameFunction()
.apply(String.valueOf(lmsSetup.institutionId));
}
}

View file

@ -1,258 +1,277 @@
/*
* 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 java.util.Collection;
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.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;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
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;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientEventPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute;
import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy
@Component
@GuiProfile
public class MonitoringClientConnection implements TemplateComposer {
private static final LocTextKey PAGE_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.title");
private static final LocTextKey EVENT_LIST_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.title");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.empty");
private static final LocTextKey LIST_COLUMN_TYPE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.type");
private static final LocTextKey LIST_COLUMN_CLIENT_TIME_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.clienttime");
private static final LocTextKey LIST_COLUMN_SERVER_TIME_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.servertime");
private static final LocTextKey LIST_COLUMN_VALUE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.value");
private static final LocTextKey LIST_COLUMN_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.text");
private static final LocTextKey CONFIRM_QUIT =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.confirm");
private final ServerPushService serverPushService;
private final PageService pageService;
private final ResourceService resourceService;
private final I18nSupport i18nSupport;
private final InstructionProcessor instructionProcessor;
private final long pollInterval;
private final int pageSize;
private final TableFilterAttribute typeFilter;
private final TableFilterAttribute textFilter =
new TableFilterAttribute(CriteriaType.TEXT, ClientEvent.FILTER_ATTR_TEXT);
protected MonitoringClientConnection(
final ServerPushService serverPushService,
final PageService pageService,
final ResourceService resourceService,
final InstructionProcessor instructionProcessor,
@Value("${sebserver.gui.webservice.poll-interval:500}") final long pollInterval,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.serverPushService = serverPushService;
this.pageService = pageService;
this.resourceService = resourceService;
this.i18nSupport = resourceService.getI18nSupport();
this.instructionProcessor = instructionProcessor;
this.pollInterval = pollInterval;
this.pageSize = pageSize;
this.typeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Domain.CLIENT_EVENT.ATTR_TYPE,
this.resourceService::clientEventTypeResources);
}
@Override
public void compose(final PageContext pageContext) {
final RestService restService = this.resourceService.getRestService();
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
final EntityKey entityKey = pageContext.getEntityKey();
final String connectionToken = pageContext.getAttribute(Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN);
if (connectionToken == null) {
pageContext.notifyUnexpectedError(new IllegalAccessException("connectionToken has null reference"));
return;
}
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(),
PAGE_TITLE_KEY);
final Exam exam = restService.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.call()
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow();
final Collection<Indicator> indicators = restService.getBuilder(GetIndicators.class)
.withQueryParam(Indicator.FILTER_ATTR_EXAM_ID, parentEntityKey.modelId)
.call()
.getOrThrow();
final RestCall<ClientConnectionData>.RestCallBuilder getConnectionData =
restService.getBuilder(GetClientConnectionData.class)
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId())
.withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken);
final ClientConnectionData connectionData = getConnectionData
.call()
.getOrThrow();
final ClientConnectionDetails clientConnectionDetails = new ClientConnectionDetails(
this.pageService,
pageContext.copyOf(content),
exam,
getConnectionData,
indicators);
this.serverPushService.runServerPush(
new ServerPushContext(content, Utils.truePredicate()),
this.pollInterval,
clientConnectionDetails::updateData,
clientConnectionDetails::updateGUI);
widgetFactory.labelLocalized(
content,
CustomVariant.TEXT_H3,
EVENT_LIST_TITLE_KEY);
// client event table for this connection
this.pageService.entityTableBuilder(restService.getRestCall(GetClientEventPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withRestCallAdapter(restCallBuilder -> restCallBuilder.withQueryParam(
ClientEvent.FILTER_ATTR_CONECTION_ID,
entityKey.modelId))
.withColumn(new ColumnDefinition<ClientEvent>(
Domain.CLIENT_EVENT.ATTR_TYPE,
LIST_COLUMN_TYPE_KEY,
this.resourceService::getEventTypeName)
.withFilter(this.typeFilter)
.sortable()
.widthProportion(2))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_EVENT.ATTR_TEXT,
LIST_COLUMN_TEXT_KEY,
ClientEvent::getText)
.withFilter(this.textFilter)
.sortable()
.withCellTooltip()
.widthProportion(4))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE,
LIST_COLUMN_VALUE_KEY,
ClientEvent::getValue)
.widthProportion(1))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_EVENT.ATTR_CLIENT_TIME,
new LocTextKey(LIST_COLUMN_CLIENT_TIME_KEY.name,
this.i18nSupport.getUsersTimeZoneTitleSuffix()),
this::getClientTime)
.sortable()
.widthProportion(1))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
new LocTextKey(LIST_COLUMN_SERVER_TIME_KEY.name,
this.i18nSupport.getUsersTimeZoneTitleSuffix()),
this::getServerTime)
.sortable()
.widthProportion(1))
.compose(pageContext.copyOf(content));
this.pageService
.pageActionBuilder(
pageContext
.clearAttributes()
.clearEntityKeys())
.newAction(ActionDefinition.MONITOR_EXAM_FROM_DETAILS)
.withEntityKey(parentEntityKey)
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER))
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_QUIT)
.withConfirm(() -> CONFIRM_QUIT)
.withExec(action -> {
this.instructionProcessor.propagateSebQuitInstruction(
exam.id,
connectionToken,
pageContext);
return action;
})
.noEventPropagation()
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER) &&
connectionData.clientConnection.status == ConnectionStatus.ACTIVE);
}
private final String getClientTime(final ClientEvent event) {
if (event == null || event.getClientTime() == null) {
return Constants.EMPTY_NOTE;
}
return this.i18nSupport
.formatDisplayTime(Utils.toDateTimeUTC(event.getClientTime()));
}
private final String getServerTime(final ClientEvent event) {
if (event == null || event.getServerTime() == null) {
return Constants.EMPTY_NOTE;
}
return this.i18nSupport
.formatDisplayTime(Utils.toDateTimeUTC(event.getServerTime()));
}
}
/*
* 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 java.util.Collection;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
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.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;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
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;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientEventPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute;
import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy
@Component
@GuiProfile
public class MonitoringClientConnection implements TemplateComposer {
private static final LocTextKey PAGE_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.title");
private static final LocTextKey EVENT_LIST_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.title");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.empty");
private static final LocTextKey LIST_COLUMN_TYPE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.type");
private static final LocTextKey LIST_COLUMN_CLIENT_TIME_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.clienttime");
private static final LocTextKey LIST_COLUMN_SERVER_TIME_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.servertime");
private static final LocTextKey LIST_COLUMN_VALUE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.value");
private static final LocTextKey LIST_COLUMN_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.text");
private static final LocTextKey CONFIRM_QUIT =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.confirm");
private final ServerPushService serverPushService;
private final PageService pageService;
private final ResourceService resourceService;
private final I18nSupport i18nSupport;
private final InstructionProcessor instructionProcessor;
private final SebClientLogDetailsPopup sebClientLogDetailsPopup;
private final long pollInterval;
private final int pageSize;
private final TableFilterAttribute typeFilter;
private final TableFilterAttribute textFilter =
new TableFilterAttribute(CriteriaType.TEXT, ClientEvent.FILTER_ATTR_TEXT);
protected MonitoringClientConnection(
final ServerPushService serverPushService,
final PageService pageService,
final ResourceService resourceService,
final InstructionProcessor instructionProcessor,
final SebClientLogDetailsPopup sebClientLogDetailsPopup,
@Value("${sebserver.gui.webservice.poll-interval:500}") final long pollInterval,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.serverPushService = serverPushService;
this.pageService = pageService;
this.resourceService = resourceService;
this.i18nSupport = resourceService.getI18nSupport();
this.instructionProcessor = instructionProcessor;
this.pollInterval = pollInterval;
this.sebClientLogDetailsPopup = sebClientLogDetailsPopup;
this.pageSize = pageSize;
this.typeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Domain.CLIENT_EVENT.ATTR_TYPE,
this.resourceService::clientEventTypeResources);
}
@Override
public void compose(final PageContext pageContext) {
final RestService restService = this.resourceService.getRestService();
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
final EntityKey entityKey = pageContext.getEntityKey();
final String connectionToken = pageContext.getAttribute(Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN);
if (connectionToken == null) {
pageContext.notifyUnexpectedError(new IllegalAccessException("connectionToken has null reference"));
return;
}
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(),
PAGE_TITLE_KEY);
final Exam exam = restService.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.call()
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow();
final Collection<Indicator> indicators = restService.getBuilder(GetIndicators.class)
.withQueryParam(Indicator.FILTER_ATTR_EXAM_ID, parentEntityKey.modelId)
.call()
.getOrThrow();
final RestCall<ClientConnectionData>.RestCallBuilder getConnectionData =
restService.getBuilder(GetClientConnectionData.class)
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId())
.withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken);
final ClientConnectionData connectionData = getConnectionData
.call()
.getOrThrow();
final ClientConnectionDetails clientConnectionDetails = new ClientConnectionDetails(
this.pageService,
pageContext.copyOf(content),
exam,
getConnectionData,
indicators);
this.serverPushService.runServerPush(
new ServerPushContext(content, Utils.truePredicate()),
this.pollInterval,
context1 -> clientConnectionDetails.updateData(),
context -> clientConnectionDetails.updateGUI());
widgetFactory.labelLocalized(
content,
CustomVariant.TEXT_H3,
EVENT_LIST_TITLE_KEY);
PageService.PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(
pageContext
.clearAttributes()
.clearEntityKeys());
// client event table for this connection
this.pageService.entityTableBuilder(restService.getRestCall(GetExtendedClientEventPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withRestCallAdapter(restCallBuilder -> restCallBuilder.withQueryParam(
ClientEvent.FILTER_ATTR_CONECTION_ID,
entityKey.modelId))
.withColumn(new ColumnDefinition<ExtendedClientEvent>(
Domain.CLIENT_EVENT.ATTR_TYPE,
LIST_COLUMN_TYPE_KEY,
this.resourceService::getEventTypeName)
.withFilter(this.typeFilter)
.sortable()
.widthProportion(2))
.withColumn(new ColumnDefinition<ExtendedClientEvent>(
Domain.CLIENT_EVENT.ATTR_TEXT,
LIST_COLUMN_TEXT_KEY,
ClientEvent::getText)
.withFilter(this.textFilter)
.sortable()
.withCellTooltip()
.widthProportion(4))
.withColumn(new ColumnDefinition<ExtendedClientEvent>(
Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE,
LIST_COLUMN_VALUE_KEY,
ClientEvent::getValue)
.widthProportion(1))
.withColumn(new ColumnDefinition<ExtendedClientEvent>(
Domain.CLIENT_EVENT.ATTR_CLIENT_TIME,
new LocTextKey(LIST_COLUMN_CLIENT_TIME_KEY.name,
this.i18nSupport.getUsersTimeZoneTitleSuffix()),
this::getClientTime)
.sortable()
.widthProportion(1))
.withColumn(new ColumnDefinition<ExtendedClientEvent>(
Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
new LocTextKey(LIST_COLUMN_SERVER_TIME_KEY.name,
this.i18nSupport.getUsersTimeZoneTitleSuffix()),
this::getServerTime)
.sortable()
.widthProportion(1))
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.LOGS_SEB_CLIENT_SHOW_DETAILS)
.withExec(action -> sebClientLogDetailsPopup.showDetails(action, t.getSingleSelectedROWData()))
.noEventPropagation()
.create())
.compose(pageContext.copyOf(content));
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_BACK_TO_OVERVIEW)
.withEntityKey(parentEntityKey)
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER))
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_QUIT)
.withConfirm(() -> CONFIRM_QUIT)
.withExec(action -> {
this.instructionProcessor.propagateSebQuitInstruction(
exam.id,
connectionToken,
pageContext);
return action;
})
.noEventPropagation()
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER) &&
connectionData.clientConnection.status == ConnectionStatus.ACTIVE);
}
private String getClientTime(final ClientEvent event) {
if (event == null || event.getClientTime() == null) {
return Constants.EMPTY_NOTE;
}
return this.i18nSupport
.formatDisplayTime(Utils.toDateTimeUTC(event.getClientTime()));
}
private String getServerTime(final ClientEvent event) {
if (event == null || event.getServerTime() == null) {
return Constants.EMPTY_NOTE;
}
return this.i18nSupport
.formatDisplayTime(Utils.toDateTimeUTC(event.getServerTime()));
}
}

View file

@ -1,355 +1,361 @@
/*
* 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 java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.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;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Tuple;
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.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.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.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
@Lazy
@Component
@GuiProfile
public class MonitoringRunningExam implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(MonitoringRunningExam.class);
private static final LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.emptySelection");
private static final LocTextKey EMPTY_ACTIVE_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.emptySelection.active");
private static final LocTextKey CONFIRM_QUIT_SELECTED =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected.confirm");
private static final LocTextKey CONFIRM_QUIT_ALL =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm");
private static final LocTextKey CONFIRM_DISABLE_SELECTED =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm");
private final ServerPushService serverPushService;
private final PageService pageService;
private final ResourceService resourceService;
private final InstructionProcessor instructionProcessor;
private final long pollInterval;
protected MonitoringRunningExam(
final ServerPushService serverPushService,
final PageService pageService,
final ResourceService resourceService,
final InstructionProcessor instructionProcessor,
@Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval) {
this.serverPushService = serverPushService;
this.pageService = pageService;
this.resourceService = resourceService;
this.instructionProcessor = instructionProcessor;
this.pollInterval = pollInterval;
}
@Override
public void compose(final PageContext pageContext) {
final RestService restService = this.resourceService.getRestService();
final EntityKey entityKey = pageContext.getEntityKey();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final Exam exam = restService.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.getOrThrow();
final Collection<Indicator> indicators = restService.getBuilder(GetIndicators.class)
.withQueryParam(Indicator.FILTER_ATTR_EXAM_ID, entityKey.modelId)
.call()
.getOrThrow();
final Composite content = this.pageService.getWidgetFactory().defaultPageLayout(
pageContext.getParent(),
new LocTextKey("sebserver.monitoring.exam", exam.name));
final Composite tablePane = new Composite(content, SWT.NONE);
tablePane.setLayout(new GridLayout());
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
gridData.heightHint = 100;
tablePane.setLayoutData(gridData);
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCall =
restService.getBuilder(GetClientConnectionDataList.class)
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId());
final ClientConnectionTable clientTable = new ClientConnectionTable(
this.pageService,
tablePane,
exam,
indicators,
restCall);
clientTable.withDefaultAction(
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(entityKey)
.create(),
this.pageService);
this.serverPushService.runServerPush(
new ServerPushContext(content, Utils.truePredicate()),
this.pollInterval,
context -> clientTable.updateValues(),
updateTableGUI(clientTable));
final BooleanSupplier privilege = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER);
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(entityKey)
.withExec(pageAction -> {
final Tuple<String> singleSelection = clientTable.getSingleSelection();
if (singleSelection == null) {
throw new PageMessageException(EMPTY_SELECTION_TEXT_KEY);
}
final PageAction copyOfPageAction = PageAction.copyOf(pageAction);
copyOfPageAction.withEntityKey(new EntityKey(
singleSelection._1,
EntityType.CLIENT_CONNECTION));
copyOfPageAction.withAttribute(
Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
singleSelection._2);
return copyOfPageAction;
})
.publishIf(privilege)
.newAction(ActionDefinition.MONITOR_EXAM_QUIT_ALL)
.withEntityKey(entityKey)
.withConfirm(() -> CONFIRM_QUIT_ALL)
.withExec(action -> this.quitSebClients(action, clientTable, true))
.noEventPropagation()
.publishIf(privilege)
.newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED)
.withEntityKey(entityKey)
.withConfirm(() -> CONFIRM_QUIT_SELECTED)
.withSelect(
() -> this.selectionForQuitInstruction(clientTable),
action -> this.quitSebClients(action, clientTable, false),
EMPTY_ACTIVE_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(privilege)
.newAction(ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION)
.withEntityKey(entityKey)
.withConfirm(() -> CONFIRM_DISABLE_SELECTED)
.withSelect(
clientTable::getSelection,
action -> this.disableSebClients(action, clientTable, false),
EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(privilege);
if (privilege.getAsBoolean()) {
if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED))
.noEventPropagation()
.create())
.publish();
} else {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED))
.noEventPropagation()
.create())
.publish();
}
if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION)
.withExec(
hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
.noEventPropagation()
.create())
.publish();
} else {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION)
.withExec(
showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
.noEventPropagation()
.create())
.publish();
}
if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED))
.noEventPropagation()
.create())
.publish();
} else {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED))
.noEventPropagation()
.create())
.publish();
}
}
}
private static final Function<PageAction, PageAction> showStateViewAction(
final ClientConnectionTable clientTable,
final ConnectionStatus status) {
return action -> {
clientTable.showStatus(status);
clientTable.removeSelection();
return action;
};
}
private static final Function<PageAction, PageAction> hideStateViewAction(
final ClientConnectionTable clientTable,
final ConnectionStatus status) {
return action -> {
clientTable.hideStatus(status);
clientTable.removeSelection();
return action;
};
}
private Set<EntityKey> selectionForQuitInstruction(final ClientConnectionTable clientTable) {
final Set<String> connectionTokens = clientTable.getConnectionTokens(
ClientConnection.getStatusPredicate(ConnectionStatus.ACTIVE),
true);
if (connectionTokens == null || connectionTokens.isEmpty()) {
return Collections.emptySet();
}
return clientTable.getSelection();
}
private PageAction quitSebClients(
final PageAction action,
final ClientConnectionTable clientTable,
final boolean all) {
this.instructionProcessor.propagateSebQuitInstruction(
clientTable.getExam().id,
statesPredicate -> clientTable.getConnectionTokens(
statesPredicate,
!all),
action.pageContext());
clientTable.removeSelection();
return action;
}
private PageAction disableSebClients(
final PageAction action,
final ClientConnectionTable clientTable,
final boolean all) {
this.instructionProcessor.disableConnection(
clientTable.getExam().id,
statesPredicate -> clientTable.getConnectionTokens(
statesPredicate,
!all),
action.pageContext());
clientTable.removeSelection();
return action;
}
private final Consumer<ServerPushContext> updateTableGUI(final ClientConnectionTable clientTable) {
return context -> {
if (!context.isDisposed()) {
try {
clientTable.updateGUI();
context.layout();
} catch (final Exception e) {
if (log.isWarnEnabled()) {
log.warn("Unexpected error while trying to update GUI: ", e);
}
}
}
};
}
}
/*
* 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 java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.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;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Tuple;
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.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.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.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
@Lazy
@Component
@GuiProfile
public class MonitoringRunningExam implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(MonitoringRunningExam.class);
private static final LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.emptySelection");
private static final LocTextKey EMPTY_ACTIVE_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.emptySelection.active");
private static final LocTextKey CONFIRM_QUIT_SELECTED =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected.confirm");
private static final LocTextKey CONFIRM_QUIT_ALL =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm");
private static final LocTextKey CONFIRM_DISABLE_SELECTED =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm");
private final ServerPushService serverPushService;
private final PageService pageService;
private final ResourceService resourceService;
private final InstructionProcessor instructionProcessor;
private final long pollInterval;
protected MonitoringRunningExam(
final ServerPushService serverPushService,
final PageService pageService,
final ResourceService resourceService,
final InstructionProcessor instructionProcessor,
@Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval) {
this.serverPushService = serverPushService;
this.pageService = pageService;
this.resourceService = resourceService;
this.instructionProcessor = instructionProcessor;
this.pollInterval = pollInterval;
}
@Override
public void compose(final PageContext pageContext) {
final RestService restService = this.resourceService.getRestService();
final EntityKey entityKey = pageContext.getEntityKey();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final Exam exam = restService.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.getOrThrow();
final Collection<Indicator> indicators = restService.getBuilder(GetIndicators.class)
.withQueryParam(Indicator.FILTER_ATTR_EXAM_ID, entityKey.modelId)
.call()
.getOrThrow();
final Composite content = this.pageService.getWidgetFactory().defaultPageLayout(
pageContext.getParent(),
new LocTextKey("sebserver.monitoring.exam", exam.name));
final Composite tablePane = new Composite(content, SWT.NONE);
tablePane.setLayout(new GridLayout());
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
gridData.heightHint = 100;
tablePane.setLayoutData(gridData);
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCall =
restService.getBuilder(GetClientConnectionDataList.class)
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId());
final ClientConnectionTable clientTable = new ClientConnectionTable(
this.pageService,
tablePane,
exam,
indicators,
restCall);
clientTable
.withDefaultAction(
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(entityKey)
.create(),
this.pageService)
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION,
ActionDefinition.MONITOR_EXAM_QUIT_SELECTED,
ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION));
this.serverPushService.runServerPush(
new ServerPushContext(content, Utils.truePredicate()),
this.pollInterval,
context -> clientTable.updateValues(),
updateTableGUI(clientTable));
final BooleanSupplier privilege = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER);
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(entityKey)
.withExec(pageAction -> {
final Tuple<String> singleSelection = clientTable.getSingleSelection();
if (singleSelection == null) {
throw new PageMessageException(EMPTY_SELECTION_TEXT_KEY);
}
final PageAction copyOfPageAction = PageAction.copyOf(pageAction);
copyOfPageAction.withEntityKey(new EntityKey(
singleSelection._1,
EntityType.CLIENT_CONNECTION));
copyOfPageAction.withAttribute(
Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
singleSelection._2);
return copyOfPageAction;
})
.publishIf(privilege, false)
.newAction(ActionDefinition.MONITOR_EXAM_QUIT_ALL)
.withEntityKey(entityKey)
.withConfirm(() -> CONFIRM_QUIT_ALL)
.withExec(action -> this.quitSebClients(action, clientTable, true))
.noEventPropagation()
.publishIf(privilege)
.newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED)
.withEntityKey(entityKey)
.withConfirm(() -> CONFIRM_QUIT_SELECTED)
.withSelect(
() -> this.selectionForQuitInstruction(clientTable),
action -> this.quitSebClients(action, clientTable, false),
EMPTY_ACTIVE_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(privilege, false)
.newAction(ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION)
.withEntityKey(entityKey)
.withConfirm(() -> CONFIRM_DISABLE_SELECTED)
.withSelect(
clientTable::getSelection,
action -> this.disableSebClients(action, clientTable, false),
EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(privilege, false);
if (privilege.getAsBoolean()) {
if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED))
.noEventPropagation()
.create())
.publish();
} else {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED))
.noEventPropagation()
.create())
.publish();
}
if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION)
.withExec(
hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
.noEventPropagation()
.create())
.publish();
} else {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION)
.withExec(
showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
.noEventPropagation()
.create())
.publish();
}
if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED))
.noEventPropagation()
.create())
.publish();
} else {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION)
.withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION)
.withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED))
.noEventPropagation()
.create())
.publish();
}
}
}
private static Function<PageAction, PageAction> showStateViewAction(
final ClientConnectionTable clientTable,
final ConnectionStatus status) {
return action -> {
clientTable.showStatus(status);
clientTable.removeSelection();
return action;
};
}
private static Function<PageAction, PageAction> hideStateViewAction(
final ClientConnectionTable clientTable,
final ConnectionStatus status) {
return action -> {
clientTable.hideStatus(status);
clientTable.removeSelection();
return action;
};
}
private Set<EntityKey> selectionForQuitInstruction(final ClientConnectionTable clientTable) {
final Set<String> connectionTokens = clientTable.getConnectionTokens(
ClientConnection.getStatusPredicate(ConnectionStatus.ACTIVE),
true);
if (connectionTokens == null || connectionTokens.isEmpty()) {
return Collections.emptySet();
}
return clientTable.getSelection();
}
private PageAction quitSebClients(
final PageAction action,
final ClientConnectionTable clientTable,
final boolean all) {
this.instructionProcessor.propagateSebQuitInstruction(
clientTable.getExam().id,
statesPredicate -> clientTable.getConnectionTokens(
statesPredicate,
!all),
action.pageContext());
clientTable.removeSelection();
return action;
}
private PageAction disableSebClients(
final PageAction action,
final ClientConnectionTable clientTable,
final boolean all) {
this.instructionProcessor.disableConnection(
clientTable.getExam().id,
statesPredicate -> clientTable.getConnectionTokens(
statesPredicate,
!all),
action.pageContext());
clientTable.removeSelection();
return action;
}
private Consumer<ServerPushContext> updateTableGUI(final ClientConnectionTable clientTable) {
return context -> {
if (!context.isDisposed()) {
try {
clientTable.updateGUI();
context.layout();
} catch (final Exception e) {
if (log.isWarnEnabled()) {
log.warn("Unexpected error while trying to update GUI: ", e);
}
}
}
};
}
}

View file

@ -1,144 +1,148 @@
/*
* 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.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
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;
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.session.GetRunningExamPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
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 MonitoringRunningExamList implements TemplateComposer {
private static final LocTextKey PAGE_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.list.title");
private final static LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.info.pleaseSelect");
private final static LocTextKey COLUMN_TITLE_NAME_KEY =
new LocTextKey("sebserver.monitoring.exam.list.column.name");
private final static LocTextKey COLUMN_TITLE_TYPE_KEY =
new LocTextKey("sebserver.monitoring.exam.list.column.type");
private final static LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.list.empty");
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
private final TableFilterAttribute typeFilter;
private final PageService pageService;
private final ResourceService resourceService;
private final int pageSize;
protected MonitoringRunningExamList(
final PageService pageService,
final ResourceService resourceService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.resourceService = resourceService;
this.pageSize = pageSize;
this.typeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Exam.FILTER_ATTR_TYPE,
this.resourceService::examTypeResources);
}
@Override
public void compose(final PageContext pageContext) {
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(),
PAGE_TITLE_KEY);
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
// table
final EntityTable<Exam> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetRunningExamPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withRowDecorator(ExamList.decorateOnExamConsistency(this.pageService))
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_NAME,
COLUMN_TITLE_NAME_KEY,
Exam::getName)
.withFilter(this.nameFilter)
.sortable())
.withColumn(new ColumnDefinition<Exam>(
Domain.EXAM.ATTR_TYPE,
COLUMN_TITLE_TYPE_KEY,
this.resourceService::localizedExamTypeName)
.withFilter(this.typeFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_START_TIME,
new LocTextKey(
"sebserver.monitoring.exam.list.column.startTime",
i18nSupport.getUsersTimeZoneTitleSuffix()),
Exam::getStartTime)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_END_TIME,
new LocTextKey(
"sebserver.monitoring.exam.list.column.endTime",
i18nSupport.getUsersTimeZoneTitleSuffix()),
Exam::getEndTime)
.sortable())
.withDefaultAction(actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_FROM_LIST)
.create())
.compose(pageContext.copyOf(content));
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER));
}
}
/*
* 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.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
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;
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.session.GetRunningExamPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
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 MonitoringRunningExamList implements TemplateComposer {
private static final LocTextKey PAGE_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.list.title");
private final static LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.info.pleaseSelect");
private final static LocTextKey COLUMN_TITLE_NAME_KEY =
new LocTextKey("sebserver.monitoring.exam.list.column.name");
private final static LocTextKey COLUMN_TITLE_TYPE_KEY =
new LocTextKey("sebserver.monitoring.exam.list.column.type");
private final static LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.list.empty");
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
private final TableFilterAttribute typeFilter;
private final PageService pageService;
private final ResourceService resourceService;
private final int pageSize;
protected MonitoringRunningExamList(
final PageService pageService,
final ResourceService resourceService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.resourceService = resourceService;
this.pageSize = pageSize;
this.typeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Exam.FILTER_ATTR_TYPE,
this.resourceService::examTypeResources);
}
@Override
public void compose(final PageContext pageContext) {
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(),
PAGE_TITLE_KEY);
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
// table
final EntityTable<Exam> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetRunningExamPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withRowDecorator(ExamList.decorateOnExamConsistency(this.pageService))
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_NAME,
COLUMN_TITLE_NAME_KEY,
Exam::getName)
.withFilter(this.nameFilter)
.sortable())
.withColumn(new ColumnDefinition<Exam>(
Domain.EXAM.ATTR_TYPE,
COLUMN_TITLE_TYPE_KEY,
this.resourceService::localizedExamTypeName)
.withFilter(this.typeFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_START_TIME,
new LocTextKey(
"sebserver.monitoring.exam.list.column.startTime",
i18nSupport.getUsersTimeZoneTitleSuffix()),
Exam::getStartTime)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_END_TIME,
new LocTextKey(
"sebserver.monitoring.exam.list.column.endTime",
i18nSupport.getUsersTimeZoneTitleSuffix()),
Exam::getEndTime)
.sortable())
.withDefaultAction(actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_FROM_LIST)
.create())
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.MONITOR_EXAM_FROM_LIST))
.compose(pageContext.copyOf(content));
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false);
}
}

View file

@ -1,367 +1,373 @@
/*
* 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 java.util.Arrays;
import java.util.Collection;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import org.eclipse.swt.widgets.Composite;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
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.PageContext.AttributeKeys;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.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.quiz.GetQuizPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck;
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 QuizDiscoveryList implements TemplateComposer {
// localized text keys
private static final LocTextKey QUIZ_DETAILS_URL_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.url");
private static final LocTextKey QUIZ_DETAILS_DESCRIPTION_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.description");
private static final LocTextKey QUIZ_DETAILS_START_TIME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.starttime");
private static final LocTextKey QUIZ_DETAILS_END_TIME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.endtime");
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.title");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.empty");
private final static LocTextKey EMPTY_SELECTION_TEXT =
new LocTextKey("sebserver.quizdiscovery.info.pleaseSelect");
private final static LocTextKey INSTITUION_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.institution");
private final static LocTextKey LMS_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.lmssetup");
private final static LocTextKey NAME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.name");
private final static LocTextKey START_TIME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.starttime");
private final static LocTextKey END_TIME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.endtime");
private final static LocTextKey DETAILS_TITLE_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.title");
private final static LocTextKey NO_IMPORT_OF_OUT_DATED_QUIZ =
new LocTextKey("sebserver.quizdiscovery.quiz.import.out.dated");
private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX =
"sebserver.quizdiscovery.quiz.details.additional.";
// filter attribute models
private final TableFilterAttribute institutionFilter;
private final TableFilterAttribute lmsFilter;
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
// dependencies
private final WidgetFactory widgetFactory;
private final ResourceService resourceService;
private final PageService pageService;
private final int pageSize;
protected QuizDiscoveryList(
final PageService pageService,
final ResourceService resourceService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.widgetFactory = pageService.getWidgetFactory();
this.resourceService = resourceService;
this.pageSize = pageSize;
this.institutionFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Entity.FILTER_ATTR_INSTITUTION,
this.resourceService::institutionResource);
this.lmsFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
LmsSetup.FILTER_ATTR_LMS_SETUP,
this.resourceService::lmsSetupResource);
}
@Override
public void compose(final PageContext pageContext) {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
// content page layout with title
final Composite content = this.widgetFactory.defaultPageLayout(
pageContext.getParent(),
TITLE_TEXT_KEY);
final PageActionBuilder actionBuilder =
this.pageService.pageActionBuilder(pageContext.clearEntityKeys());
final BooleanSupplier isSebAdmin =
() -> currentUser.get().hasRole(UserRole.SEB_SERVER_ADMIN);
final Function<String, String> institutionNameFunction =
this.resourceService.getInstitutionNameFunction();
// table
final EntityTable<QuizData> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetQuizPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withColumnIf(
isSebAdmin,
() -> new ColumnDefinition<QuizData>(
QuizData.QUIZ_ATTR_INSTITUION_ID,
INSTITUION_TEXT_KEY,
quiz -> institutionNameFunction
.apply(String.valueOf(quiz.institutionId)))
.withFilter(this.institutionFilter))
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_LMS_SETUP_ID,
LMS_TEXT_KEY,
quizDataLmsSetupNameFunction(this.resourceService))
.withFilter(this.lmsFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_NAME,
NAME_TEXT_KEY,
QuizData::getName)
.withFilter(this.nameFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_START_TIME,
new LocTextKey(
START_TIME_TEXT_KEY.name,
i18nSupport.getUsersTimeZoneTitleSuffix()),
QuizData::getStartTime)
.withFilter(new TableFilterAttribute(
CriteriaType.DATE,
QuizData.FILTER_ATTR_START_TIME,
Utils.toDateTimeUTC(Utils.getMillisecondsNow())
.minusYears(1)
.toString()))
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_END_TIME,
new LocTextKey(
END_TIME_TEXT_KEY.name,
i18nSupport.getUsersTimeZoneTitleSuffix()),
QuizData::getEndTime)
.sortable())
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.QUIZ_DISCOVERY_SHOW_DETAILS)
.withExec(action -> this.showDetails(
action,
t.getSingleSelectedROWData(),
institutionNameFunction))
.noEventPropagation()
.create())
.compose(pageContext.copyOf(content));
// propagate content actions to action-pane
final GrantCheck examGrant = currentUser.grantCheck(EntityType.EXAM);
actionBuilder
// Removed as discussed in SEBSERV-52
// .newAction(ActionDefinition.LMS_SETUP_NEW)
// .publishIf(lmsSetupGrant::iw)
.newAction(ActionDefinition.QUIZ_DISCOVERY_SHOW_DETAILS)
.withSelect(
table::getSelection,
action -> this.showDetails(
action,
table.getSingleSelectedROWData(),
institutionNameFunction),
EMPTY_SELECTION_TEXT)
.noEventPropagation()
.publishIf(table::hasAnyContent)
.newAction(ActionDefinition.QUIZ_DISCOVERY_EXAM_IMPORT)
.withSelect(
table::getSelection,
action -> this.importQuizData(action, table),
EMPTY_SELECTION_TEXT)
.publishIf(() -> examGrant.im() && table.hasAnyContent());
}
private static Function<QuizData, String> quizDataLmsSetupNameFunction(final ResourceService resourceService) {
return quizzData -> resourceService.getLmsSetupNameFunction()
.apply(String.valueOf(quizzData.lmsSetupId));
}
private PageAction importQuizData(final PageAction action, final EntityTable<QuizData> table) {
action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData();
if (selectedROWData.endTime != null) {
final DateTime now = DateTime.now(DateTimeZone.UTC);
if (selectedROWData.endTime.isBefore(now)) {
throw new PageMessageException(NO_IMPORT_OF_OUT_DATED_QUIZ);
}
}
return action
.withEntityKey(action.getSingleSelection())
.withParentEntityKey(new EntityKey(selectedROWData.lmsSetupId, EntityType.LMS_SETUP))
.withAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA, "true");
}
private PageAction showDetails(
final PageAction action,
final QuizData quizData,
final Function<String, String> institutionNameFunction) {
action.getSingleSelection();
final ModalInputDialog<Void> dialog = new ModalInputDialog<Void>(
action.pageContext().getParent().getShell(),
this.widgetFactory)
.setLargeDialogWidth();
dialog.open(
DETAILS_TITLE_TEXT_KEY,
action.pageContext(),
pc -> createDetailsForm(quizData, pc, institutionNameFunction));
return action;
}
private static final Collection<String> ADDITIONAL_HTML_ATTRIBUTES = Arrays.asList(
"course_summary");
private void createDetailsForm(
final QuizData quizData,
final PageContext pc,
final Function<String, String> institutionNameFunction) {
final Composite parent = pc.getParent();
final Composite grid = this.widgetFactory.createPopupScrollComposite(parent);
final FormBuilder formbuilder = this.pageService.formBuilder(pc.copyOf(grid))
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(true)
.addFieldIf(
() -> this.resourceService.getCurrentUser().get().hasRole(UserRole.SEB_SERVER_ADMIN),
() -> FormBuilder.text(
QuizData.QUIZ_ATTR_INSTITUION_ID,
INSTITUION_TEXT_KEY,
institutionNameFunction.apply(quizData.getModelId())))
.addField(FormBuilder.singleSelection(
QuizData.QUIZ_ATTR_LMS_SETUP_ID,
LMS_TEXT_KEY,
String.valueOf(quizData.lmsSetupId),
() -> this.resourceService.lmsSetupResource()))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
NAME_TEXT_KEY,
quizData.name))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_DESCRIPTION,
QUIZ_DETAILS_DESCRIPTION_TEXT_KEY,
quizData.description)
.asHTML())
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_TIME,
QUIZ_DETAILS_START_TIME_TEXT_KEY,
this.widgetFactory.getI18nSupport().formatDisplayDateWithTimeZone(quizData.startTime)))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_END_TIME,
QUIZ_DETAILS_END_TIME_TEXT_KEY,
this.widgetFactory.getI18nSupport().formatDisplayDateWithTimeZone(quizData.endTime)))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_URL,
QUIZ_DETAILS_URL_TEXT_KEY,
quizData.startURL));
if (!quizData.additionalAttributes.isEmpty()) {
quizData.additionalAttributes
.entrySet()
.stream()
.forEach(entry -> {
LocTextKey titleKey = new LocTextKey(TEXT_KEY_ADDITIONAL_ATTR_PREFIX + entry.getKey());
if (!this.pageService.getI18nSupport().hasText(titleKey)) {
titleKey = new LocTextKey(entry.getKey());
}
formbuilder
.addField(FormBuilder.text(
entry.getKey(),
titleKey,
toAdditionalValue(entry.getKey(), entry.getValue()))
.asHTML(ADDITIONAL_HTML_ATTRIBUTES.contains(entry.getKey())));
});
}
formbuilder.build();
}
private String toAdditionalValue(final String name, final String value) {
if ("timecreated".equals(name)) {
try {
return this.pageService
.getI18nSupport()
.formatDisplayDate(Utils.toDateTimeUTCUnix(Long.parseLong(value)));
} catch (final Exception e) {
return value;
}
} else if ("timelimit".equals(name)) {
try {
return this.pageService
.getI18nSupport()
.formatDisplayTime(Utils.toDateTimeUTCUnix(Long.parseLong(value)));
} catch (final Exception e) {
return value;
}
} else {
return value;
}
}
}
/*
* 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 java.util.Arrays;
import java.util.Collection;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.Constants;
import org.eclipse.swt.widgets.Composite;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
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.PageContext.AttributeKeys;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.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.quiz.GetQuizPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck;
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 QuizDiscoveryList implements TemplateComposer {
// localized text keys
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.title");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.empty");
private final static LocTextKey EMPTY_SELECTION_TEXT =
new LocTextKey("sebserver.quizdiscovery.info.pleaseSelect");
private final static LocTextKey INSTITUTION_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.institution");
private final static LocTextKey LMS_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.lmssetup");
private final static LocTextKey NAME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.name");
private final static LocTextKey START_TIME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.starttime");
private final static LocTextKey END_TIME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.list.column.endtime");
private final static LocTextKey DETAILS_TITLE_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.title");
private static final LocTextKey QUIZ_DETAILS_URL_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.url");
private static final LocTextKey QUIZ_DETAILS_INSTITUTION_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.institution");
private static final LocTextKey QUIZ_DETAILS_LMS_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.lmssetup");
private static final LocTextKey QUIZ_DETAILS_NAME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.name");
private static final LocTextKey QUIZ_DETAILS_DESCRIPTION_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.description");
private static final LocTextKey QUIZ_DETAILS_START_TIME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.starttime");
private static final LocTextKey QUIZ_DETAILS_END_TIME_TEXT_KEY =
new LocTextKey("sebserver.quizdiscovery.quiz.details.endtime");
private final static LocTextKey NO_IMPORT_OF_OUT_DATED_QUIZ =
new LocTextKey("sebserver.quizdiscovery.quiz.import.out.dated");
private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX =
"sebserver.quizdiscovery.quiz.details.additional.";
// filter attribute models
private final TableFilterAttribute institutionFilter;
private final TableFilterAttribute lmsFilter;
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
// dependencies
private final WidgetFactory widgetFactory;
private final ResourceService resourceService;
private final PageService pageService;
private final int pageSize;
protected QuizDiscoveryList(
final PageService pageService,
final ResourceService resourceService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.widgetFactory = pageService.getWidgetFactory();
this.resourceService = resourceService;
this.pageSize = pageSize;
this.institutionFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Entity.FILTER_ATTR_INSTITUTION,
this.resourceService::institutionResource);
this.lmsFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
LmsSetup.FILTER_ATTR_LMS_SETUP,
this.resourceService::lmsSetupResource);
}
@Override
public void compose(final PageContext pageContext) {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
// content page layout with title
final Composite content = this.widgetFactory.defaultPageLayout(
pageContext.getParent(),
TITLE_TEXT_KEY);
final PageActionBuilder actionBuilder =
this.pageService.pageActionBuilder(pageContext.clearEntityKeys());
final BooleanSupplier isSebAdmin =
() -> currentUser.get().hasRole(UserRole.SEB_SERVER_ADMIN);
final Function<String, String> institutionNameFunction =
this.resourceService.getInstitutionNameFunction();
// table
final EntityTable<QuizData> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetQuizPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withColumnIf(
isSebAdmin,
() -> new ColumnDefinition<QuizData>(
QuizData.QUIZ_ATTR_INSTITUTION_ID,
INSTITUTION_TEXT_KEY,
quiz -> institutionNameFunction
.apply(String.valueOf(quiz.institutionId)))
.withFilter(this.institutionFilter))
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_LMS_SETUP_ID,
LMS_TEXT_KEY,
quizDataLmsSetupNameFunction(this.resourceService))
.withFilter(this.lmsFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_NAME,
NAME_TEXT_KEY,
QuizData::getName)
.withFilter(this.nameFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_START_TIME,
new LocTextKey(
START_TIME_TEXT_KEY.name,
i18nSupport.getUsersTimeZoneTitleSuffix()),
QuizData::getStartTime)
.withFilter(new TableFilterAttribute(
CriteriaType.DATE,
QuizData.FILTER_ATTR_START_TIME,
Utils.toDateTimeUTC(Utils.getMillisecondsNow())
.minusYears(1)
.toString()))
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_END_TIME,
new LocTextKey(
END_TIME_TEXT_KEY.name,
i18nSupport.getUsersTimeZoneTitleSuffix()),
QuizData::getEndTime)
.sortable())
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.QUIZ_DISCOVERY_SHOW_DETAILS)
.withExec(action -> this.showDetails(
action,
t.getSingleSelectedROWData(),
institutionNameFunction))
.noEventPropagation()
.create())
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.QUIZ_DISCOVERY_SHOW_DETAILS,
ActionDefinition.QUIZ_DISCOVERY_EXAM_IMPORT))
.compose(pageContext.copyOf(content));
// propagate content actions to action-pane
final GrantCheck examGrant = currentUser.grantCheck(EntityType.EXAM);
actionBuilder
.newAction(ActionDefinition.QUIZ_DISCOVERY_SHOW_DETAILS)
.withSelect(
table::getSelection,
action -> this.showDetails(
action,
table.getSingleSelectedROWData(),
institutionNameFunction),
EMPTY_SELECTION_TEXT)
.noEventPropagation()
.publishIf(table::hasAnyContent, false)
.newAction(ActionDefinition.QUIZ_DISCOVERY_EXAM_IMPORT)
.withSelect(
table::getSelection,
action -> this.importQuizData(action, table),
EMPTY_SELECTION_TEXT)
.publishIf(() -> examGrant.im() && table.hasAnyContent(), false);
}
private static Function<QuizData, String> quizDataLmsSetupNameFunction(final ResourceService resourceService) {
return quizzData -> resourceService.getLmsSetupNameFunction()
.apply(String.valueOf(quizzData.lmsSetupId));
}
private PageAction importQuizData(final PageAction action, final EntityTable<QuizData> table) {
action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData();
if (selectedROWData.endTime != null) {
final DateTime now = DateTime.now(DateTimeZone.UTC);
if (selectedROWData.endTime.isBefore(now)) {
throw new PageMessageException(NO_IMPORT_OF_OUT_DATED_QUIZ);
}
}
return action
.withEntityKey(action.getSingleSelection())
.withParentEntityKey(new EntityKey(selectedROWData.lmsSetupId, EntityType.LMS_SETUP))
.withAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA, Constants.TRUE_STRING);
}
private PageAction showDetails(
final PageAction action,
final QuizData quizData,
final Function<String, String> institutionNameFunction) {
action.getSingleSelection();
final ModalInputDialog<Void> dialog = new ModalInputDialog<Void>(
action.pageContext().getParent().getShell(),
this.widgetFactory)
.setLargeDialogWidth();
dialog.open(
DETAILS_TITLE_TEXT_KEY,
action.pageContext(),
pc -> createDetailsForm(quizData, pc, institutionNameFunction));
return action;
}
private static final Collection<String> ADDITIONAL_HTML_ATTRIBUTES = Arrays.asList(
"course_summary");
private void createDetailsForm(
final QuizData quizData,
final PageContext pc,
final Function<String, String> institutionNameFunction) {
final Composite parent = pc.getParent();
final Composite grid = this.widgetFactory.createPopupScrollComposite(parent);
final FormBuilder formbuilder = this.pageService.formBuilder(pc.copyOf(grid))
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(true)
.addFieldIf(
() -> this.resourceService.getCurrentUser().get().hasRole(UserRole.SEB_SERVER_ADMIN),
() -> FormBuilder.text(
QuizData.QUIZ_ATTR_INSTITUTION_ID,
QUIZ_DETAILS_INSTITUTION_TEXT_KEY,
institutionNameFunction.apply(quizData.getModelId())))
.addField(FormBuilder.singleSelection(
QuizData.QUIZ_ATTR_LMS_SETUP_ID,
QUIZ_DETAILS_LMS_TEXT_KEY,
String.valueOf(quizData.lmsSetupId),
this.resourceService::lmsSetupResource))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
QUIZ_DETAILS_NAME_TEXT_KEY,
quizData.name))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_DESCRIPTION,
QUIZ_DETAILS_DESCRIPTION_TEXT_KEY,
quizData.description)
.asHTML())
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_TIME,
QUIZ_DETAILS_START_TIME_TEXT_KEY,
this.widgetFactory.getI18nSupport().formatDisplayDateWithTimeZone(quizData.startTime)))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_END_TIME,
QUIZ_DETAILS_END_TIME_TEXT_KEY,
this.widgetFactory.getI18nSupport().formatDisplayDateWithTimeZone(quizData.endTime)))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_URL,
QUIZ_DETAILS_URL_TEXT_KEY,
quizData.startURL));
if (!quizData.additionalAttributes.isEmpty()) {
quizData.additionalAttributes
.forEach((key, value) -> {
LocTextKey titleKey = new LocTextKey(TEXT_KEY_ADDITIONAL_ATTR_PREFIX + key);
if (!this.pageService.getI18nSupport().hasText(titleKey)) {
titleKey = new LocTextKey(key);
}
formbuilder
.addField(FormBuilder.text(
key,
titleKey,
toAdditionalValue(key, value))
.asHTML(ADDITIONAL_HTML_ATTRIBUTES.contains(key)));
});
}
formbuilder.build();
}
private String toAdditionalValue(final String name, final String value) {
if (QuizData.ATTR_ADDITIONAL_CREATION_TIME.equals(name)) {
try {
return this.pageService
.getI18nSupport()
.formatDisplayDate(Utils.toDateTimeUTCUnix(Long.parseLong(value)));
} catch (final Exception e) {
return value;
}
} else if (QuizData.ATTR_ADDITIONAL_TIME_LIMIT.equals(name)) {
try {
return this.pageService
.getI18nSupport()
.formatDisplayTime(Utils.toDateTimeUTCUnix(Long.parseLong(value)));
} catch (final Exception e) {
return value;
}
} else {
return value;
}
}
}

View file

@ -0,0 +1,235 @@
/*
* 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 ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
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.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnection;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Lazy
@Component
@GuiProfile
public class SebClientLogDetailsPopup {
private static final Logger log = LoggerFactory.getLogger(SebClientLogDetailsPopup.class);
private static final LocTextKey DETAILS_TITLE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.details.title");
private static final LocTextKey DETAILS_EVENT_TILE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.details.event.title");
private static final LocTextKey DETAILS_CONNECTION_TILE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.details.connection.title");
private static final LocTextKey DETAILS_EXAM_TILE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.details.exam.title");
private static final LocTextKey FORM_TYPE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.type");
private static final LocTextKey FORM_SERVERTIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.server-time");
private static final LocTextKey FORM_CLIENTTIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.client-time");
private static final LocTextKey FORM_VALUE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.value");
private static final LocTextKey FORM_MESSAGE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.message");
private static final LocTextKey FORM_SESSION_ID_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.connection.session-id");
private static final LocTextKey FORM_ADDRESS_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.connection.address");
private static final LocTextKey FORM_TOKEN_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.connection.token");
private static final LocTextKey FORM_STATUS_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.connection.status");
private static final LocTextKey FORM_EXAM_NAME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.name");
private static final LocTextKey FORM_DESC_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.description");
private static final LocTextKey FORM_EXAM_TYPE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.type");
private static final LocTextKey FORM_START_TIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.startTime");
private static final LocTextKey FORM_END_TIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.endTime");
private final PageService pageService;
private final ResourceService resourceService;
private final RestService restService;
private final I18nSupport i18nSupport;
private final WidgetFactory widgetFactory;
public SebClientLogDetailsPopup(
final PageService pageService,
final WidgetFactory widgetFactory) {
this.pageService = pageService;
this.widgetFactory = widgetFactory;
this.resourceService = pageService.getResourceService();
this.restService = pageService.getRestService();
this.i18nSupport = pageService.getI18nSupport();
}
PageAction showDetails(final PageAction action, final ExtendedClientEvent clientEvent) {
action.getSingleSelection();
final ModalInputDialog<Void> dialog = new ModalInputDialog<>(
action.pageContext().getParent().getShell(),
this.widgetFactory);
dialog.setDialogWidth(600);
dialog.open(
DETAILS_TITLE_TEXT_KEY,
action.pageContext(),
pc -> createDetailsForm(clientEvent, pc));
return action;
}
private void createDetailsForm(final ExtendedClientEvent clientEvent, final PageContext pc) {
final Composite parent = pc.getParent();
final Composite content = this.widgetFactory.createPopupScrollComposite(parent);
// Event Details Title
this.widgetFactory.labelLocalized(
content,
WidgetFactory.CustomVariant.TEXT_H3,
DETAILS_EVENT_TILE_TEXT_KEY);
PageContext formContext = pc.copyOf(content);
this.pageService.formBuilder(formContext)
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(true)
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.TYPE_NAME,
FORM_TYPE_TEXT_KEY,
this.resourceService.getEventTypeName(clientEvent)))
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.ATTR_CLIENT_TIME,
FORM_CLIENTTIME_TEXT_KEY,
this.i18nSupport.formatDisplayDateTime(clientEvent.clientTime) + " " +
this.i18nSupport.getUsersTimeZoneTitleSuffix()))
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
FORM_SERVERTIME_TEXT_KEY,
this.i18nSupport.formatDisplayDateTime(clientEvent.serverTime) + " " +
this.i18nSupport.getUsersTimeZoneTitleSuffix()))
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE,
FORM_VALUE_TEXT_KEY,
(clientEvent.numValue != null)
? String.valueOf(clientEvent.numValue)
: Constants.EMPTY_NOTE))
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.ATTR_TEXT,
FORM_MESSAGE_TEXT_KEY,
clientEvent.text)
.asArea())
.build();
// SEB Client Connection Title
this.widgetFactory.labelLocalized(
content,
WidgetFactory.CustomVariant.TEXT_H3,
DETAILS_CONNECTION_TILE_TEXT_KEY);
final ClientConnection connection = this.restService.getBuilder(GetClientConnection.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(clientEvent.connectionId))
.call()
.get(
error -> log.error("Failed to get ClientConnection for id {}", clientEvent.connectionId, error),
() -> ClientConnection.EMPTY_CLIENT_CONNECTION);
this.pageService.formBuilder(formContext)
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(true)
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
FORM_SESSION_ID_TEXT_KEY,
connection.userSessionId))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS,
FORM_ADDRESS_TEXT_KEY,
connection.clientAddress))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
FORM_TOKEN_TEXT_KEY,
connection.connectionToken))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_STATUS,
FORM_STATUS_TEXT_KEY,
this.resourceService.localizedClientConnectionStatusName(connection.status)))
.build();
// Exam Details Title
this.widgetFactory.labelLocalized(
content,
WidgetFactory.CustomVariant.TEXT_H3,
DETAILS_EXAM_TILE_TEXT_KEY);
final Exam exam = this.restService.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(clientEvent.examId))
.call()
.get(
error -> log.error("Failed to get Exam for id {}", clientEvent.examId, error),
() -> Exam.EMPTY_EXAM);
this.pageService.formBuilder(formContext)
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(true)
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
FORM_EXAM_NAME_TEXT_KEY,
exam.name))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_DESCRIPTION,
FORM_DESC_TEXT_KEY,
exam.description))
.addField(FormBuilder.text(
Domain.EXAM.ATTR_TYPE,
FORM_EXAM_TYPE_TEXT_KEY,
this.resourceService.localizedExamTypeName(exam)))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_TIME,
FORM_START_TIME_TEXT_KEY,
this.i18nSupport.formatDisplayDateWithTimeZone(exam.startTime)))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_END_TIME,
FORM_END_TIME_TEXT_KEY,
this.i18nSupport.formatDisplayDateWithTimeZone(exam.endTime)))
.build();
}
}

View file

@ -1,387 +1,205 @@
/*
* 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 java.util.Map;
import java.util.function.Function;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnection;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy
@Component
@GuiProfile
public class SebClientLogs implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(SebClientLogs.class);
private static final LocTextKey DETAILS_TITLE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.details.title");
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.title");
private static final LocTextKey EMPTY_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.empty");
private static final LocTextKey EXAM_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.exam");
private static final LocTextKey CLIENT_SESSION_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.client-session");
private static final LocTextKey TYPE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.type");
private static final LocTextKey TIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.time");
private static final LocTextKey VALUE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.value");
private static final LocTextKey DETAILS_EVENT_TILE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.details.event.title");
private static final LocTextKey DETAILS_CONNECTION_TILE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.details.connection.title");
private static final LocTextKey DETAILS_EXAM_TILE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.details.exam.title");
private static final LocTextKey FORM_TYPE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.type");
private static final LocTextKey FORM_SERVERTIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.server-time");
private static final LocTextKey FORM_CLIENTTIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.client-time");
private static final LocTextKey FORM_VALUE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.value");
private static final LocTextKey FORM_MESSAGE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.message");
private static final LocTextKey FORM_SESSION_ID_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.connection.session-id");
private static final LocTextKey FORM_ADDRESS_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.connection.address");
private static final LocTextKey FORM_TOKEN_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.connection.token");
private static final LocTextKey FORM_STATUS_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.connection.status");
private static final LocTextKey FORM_EXAM_NAME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.name");
private static final LocTextKey FORM_DESC_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.description");
private static final LocTextKey FORM_EXAM_TYPE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.type");
private static final LocTextKey FORM_START_TIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.startTime");
private static final LocTextKey FORM_END_TIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.form.column.exam.endTime");
private final static LocTextKey EMPTY_SELECTION_TEXT =
new LocTextKey("sebserver.seblogs.info.pleaseSelect");
private final TableFilterAttribute examFilter;
private final TableFilterAttribute clientSessionFilter;
private final TableFilterAttribute eventTypeFilter;
private final PageService pageService;
private final ResourceService resourceService;
private final RestService restService;
private final I18nSupport i18nSupport;
private final WidgetFactory widgetFactory;
private final int pageSize;
public SebClientLogs(
final PageService pageService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.resourceService = pageService.getResourceService();
this.restService = this.resourceService.getRestService();
this.i18nSupport = this.resourceService.getI18nSupport();
this.widgetFactory = pageService.getWidgetFactory();
this.pageSize = pageSize;
this.examFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
ExtendedClientEvent.FILTER_ATTRIBUTE_EXAM,
this.resourceService::getExamLogSelectionResources);
this.clientSessionFilter = new TableFilterAttribute(
CriteriaType.TEXT,
ClientConnection.FILTER_ATTR_SESSION_ID);
this.eventTypeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
ClientEvent.FILTER_ATTR_TYPE,
this.resourceService::clientEventTypeResources);
}
@Override
public void compose(final PageContext pageContext) {
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
// 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<ExtendedClientEvent> table = this.pageService.entityTableBuilder(
this.restService.getRestCall(GetExtendedClientEventPage.class))
.withEmptyMessage(EMPTY_TEXT_KEY)
.withPaging(this.pageSize)
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_EXAM_ID,
EXAM_TEXT_KEY,
examNameFunction())
.withFilter(this.examFilter)
.widthProportion(2))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
CLIENT_SESSION_TEXT_KEY,
ExtendedClientEvent::getUserSessionId)
.withFilter(this.clientSessionFilter)
.sortable()
.widthProportion(2))
.withColumn(new ColumnDefinition<ExtendedClientEvent>(
Domain.CLIENT_EVENT.TYPE_NAME,
TYPE_TEXT_KEY,
this.resourceService::getEventTypeName)
.withFilter(this.eventTypeFilter)
.sortable()
.widthProportion(1))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
new LocTextKey(
TIME_TEXT_KEY.name,
this.i18nSupport.getUsersTimeZoneTitleSuffix()),
this::getEventTime)
.withFilter(new TableFilterAttribute(
CriteriaType.DATE_TIME_RANGE,
ClientEvent.FILTER_ATTR_SERVER_TIME_FROM_TO,
Utils.toDateTimeUTC(Utils.getMillisecondsNow())
.minusYears(1)
.toString()))
.sortable()
.widthProportion(2))
.withColumn(new ColumnDefinition<ExtendedClientEvent>(
Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE,
VALUE_TEXT_KEY,
clientEvent -> (clientEvent.numValue != null)
? String.valueOf(clientEvent.numValue)
: Constants.EMPTY_NOTE)
.widthProportion(1))
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.LOGS_SEB_CLIENT_SHOW_DETAILS)
.withExec(action -> this.showDetails(action, t.getSingleSelectedROWData()))
.noEventPropagation()
.create())
.compose(pageContext.copyOf(content));
actionBuilder
.newAction(ActionDefinition.LOGS_SEB_CLIENT_SHOW_DETAILS)
.withSelect(
table::getSelection,
action -> this.showDetails(action, table.getSingleSelectedROWData()),
EMPTY_SELECTION_TEXT)
.noEventPropagation()
.publishIf(table::hasAnyContent);
}
private PageAction showDetails(final PageAction action, final ExtendedClientEvent clientEvent) {
action.getSingleSelection();
final ModalInputDialog<Void> dialog = new ModalInputDialog<>(
action.pageContext().getParent().getShell(),
this.widgetFactory);
dialog.setDialogWidth(600);
dialog.open(
DETAILS_TITLE_TEXT_KEY,
action.pageContext(),
pc -> createDetailsForm(clientEvent, pc));
return action;
}
private void createDetailsForm(final ExtendedClientEvent clientEvent, final PageContext pc) {
final Composite parent = pc.getParent();
final Composite content = this.widgetFactory.createPopupScrollComposite(parent);
// Event Details Title
this.widgetFactory.labelLocalized(
content,
CustomVariant.TEXT_H3,
DETAILS_EVENT_TILE_TEXT_KEY);
this.pageService.formBuilder(pc.copyOf(content))
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(true)
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.TYPE_NAME,
FORM_TYPE_TEXT_KEY,
this.resourceService.getEventTypeName(clientEvent)))
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.ATTR_CLIENT_TIME,
FORM_CLIENTTIME_TEXT_KEY,
this.i18nSupport.formatDisplayDateTime(clientEvent.clientTime) + " " +
this.i18nSupport.getUsersTimeZoneTitleSuffix()))
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
FORM_SERVERTIME_TEXT_KEY,
this.i18nSupport.formatDisplayDateTime(clientEvent.serverTime) + " " +
this.i18nSupport.getUsersTimeZoneTitleSuffix()))
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE,
FORM_VALUE_TEXT_KEY,
(clientEvent.numValue != null)
? String.valueOf(clientEvent.numValue)
: Constants.EMPTY_NOTE))
.addField(FormBuilder.text(
Domain.CLIENT_EVENT.ATTR_TEXT,
FORM_MESSAGE_TEXT_KEY,
clientEvent.text)
.asArea())
.build();
// SEB Client Connection Title
this.widgetFactory.labelLocalized(
content,
CustomVariant.TEXT_H3,
DETAILS_CONNECTION_TILE_TEXT_KEY);
final ClientConnection connection = this.restService.getBuilder(GetClientConnection.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(clientEvent.connectionId))
.call()
.get(
error -> log.error("Failed to get ClientConnection for id {}", clientEvent.connectionId, error),
() -> ClientConnection.EMPTY_CLIENT_CONNECTION);
this.pageService.formBuilder(pc.copyOf(content))
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(true)
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
FORM_SESSION_ID_TEXT_KEY,
connection.userSessionId))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS,
FORM_ADDRESS_TEXT_KEY,
connection.clientAddress))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
FORM_TOKEN_TEXT_KEY,
connection.connectionToken))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_STATUS,
FORM_STATUS_TEXT_KEY,
this.resourceService.localizedClientConnectionStatusName(connection.status)))
.build();
// Exam Details Title
this.widgetFactory.labelLocalized(
content,
CustomVariant.TEXT_H3,
DETAILS_EXAM_TILE_TEXT_KEY);
final Exam exam = this.restService.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(clientEvent.examId))
.call()
.get(
error -> log.error("Failed to get Exam for id {}", clientEvent.examId, error),
() -> Exam.EMPTY_EXAM);
this.pageService.formBuilder(pc.copyOf(content))
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(true)
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
FORM_EXAM_NAME_TEXT_KEY,
exam.name))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_DESCRIPTION,
FORM_DESC_TEXT_KEY,
exam.description))
.addField(FormBuilder.text(
Domain.EXAM.ATTR_TYPE,
FORM_EXAM_TYPE_TEXT_KEY,
this.resourceService.localizedExamTypeName(exam)))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_TIME,
FORM_START_TIME_TEXT_KEY,
this.i18nSupport.formatDisplayDateWithTimeZone(exam.startTime)))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_END_TIME,
FORM_END_TIME_TEXT_KEY,
this.i18nSupport.formatDisplayDateWithTimeZone(exam.endTime)))
.build();
}
private Function<ExtendedClientEvent, String> examNameFunction() {
final Map<Long, String> examNameMapping = this.resourceService.getExamNameMapping();
return event -> examNameMapping.get(event.examId);
}
private final String getEventTime(final ExtendedClientEvent event) {
if (event == null || event.serverTime == null) {
return Constants.EMPTY_NOTE;
}
return this.i18nSupport
.formatDisplayDateTime(Utils.toDateTimeUTC(event.serverTime));
}
}
/*
* 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 ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.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.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import org.eclipse.swt.widgets.Composite;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.function.Function;
@Lazy
@Component
@GuiProfile
public class SebClientLogs implements TemplateComposer {
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.title");
private static final LocTextKey EMPTY_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.empty");
private static final LocTextKey EXAM_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.exam");
private static final LocTextKey CLIENT_SESSION_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.client-session");
private static final LocTextKey TYPE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.type");
private static final LocTextKey TIME_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.time");
private static final LocTextKey VALUE_TEXT_KEY =
new LocTextKey("sebserver.seblogs.list.column.value");
private final static LocTextKey EMPTY_SELECTION_TEXT =
new LocTextKey("sebserver.seblogs.info.pleaseSelect");
private final TableFilterAttribute examFilter;
private final TableFilterAttribute clientSessionFilter;
private final TableFilterAttribute eventTypeFilter;
private final PageService pageService;
private final ResourceService resourceService;
private final RestService restService;
private final I18nSupport i18nSupport;
private final SebClientLogDetailsPopup sebClientLogDetailsPopup;
private final int pageSize;
public SebClientLogs(
final PageService pageService,
final SebClientLogDetailsPopup sebClientLogDetailsPopup,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.resourceService = pageService.getResourceService();
this.restService = this.resourceService.getRestService();
this.i18nSupport = this.resourceService.getI18nSupport();
this.sebClientLogDetailsPopup = sebClientLogDetailsPopup;
this.pageSize = pageSize;
this.examFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
ExtendedClientEvent.FILTER_ATTRIBUTE_EXAM,
this.resourceService::getExamLogSelectionResources);
this.clientSessionFilter = new TableFilterAttribute(
CriteriaType.TEXT,
ClientConnection.FILTER_ATTR_SESSION_ID);
this.eventTypeFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
ClientEvent.FILTER_ATTR_TYPE,
this.resourceService::clientEventTypeResources);
}
@Override
public void compose(final PageContext pageContext) {
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
// 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<ExtendedClientEvent> table = this.pageService.entityTableBuilder(
this.restService.getRestCall(GetExtendedClientEventPage.class))
.withEmptyMessage(EMPTY_TEXT_KEY)
.withPaging(this.pageSize)
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_EXAM_ID,
EXAM_TEXT_KEY,
examNameFunction())
.withFilter(this.examFilter)
.widthProportion(2))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
CLIENT_SESSION_TEXT_KEY,
ExtendedClientEvent::getUserSessionId)
.withFilter(this.clientSessionFilter)
.sortable()
.widthProportion(2))
.withColumn(new ColumnDefinition<ExtendedClientEvent>(
Domain.CLIENT_EVENT.TYPE_NAME,
TYPE_TEXT_KEY,
this.resourceService::getEventTypeName)
.withFilter(this.eventTypeFilter)
.sortable()
.widthProportion(1))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
new LocTextKey(
TIME_TEXT_KEY.name,
this.i18nSupport.getUsersTimeZoneTitleSuffix()),
this::getEventTime)
.withFilter(new TableFilterAttribute(
CriteriaType.DATE_TIME_RANGE,
ClientEvent.FILTER_ATTR_SERVER_TIME_FROM_TO,
Utils.toDateTimeUTC(Utils.getMillisecondsNow())
.minusYears(1)
.toString()))
.sortable()
.widthProportion(2))
.withColumn(new ColumnDefinition<ExtendedClientEvent>(
Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE,
VALUE_TEXT_KEY,
clientEvent -> (clientEvent.numValue != null)
? String.valueOf(clientEvent.numValue)
: Constants.EMPTY_NOTE)
.widthProportion(1))
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.LOGS_SEB_CLIENT_SHOW_DETAILS)
.withExec(action -> sebClientLogDetailsPopup.showDetails(action, t.getSingleSelectedROWData()))
.noEventPropagation()
.create())
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.LOGS_SEB_CLIENT_SHOW_DETAILS))
.compose(pageContext.copyOf(content));
actionBuilder
.newAction(ActionDefinition.LOGS_SEB_CLIENT_SHOW_DETAILS)
.withSelect(
table::getSelection,
action -> sebClientLogDetailsPopup.showDetails(action, table.getSingleSelectedROWData()),
EMPTY_SELECTION_TEXT)
.noEventPropagation()
.publishIf(table::hasAnyContent, false);
}
private Function<ExtendedClientEvent, String> examNameFunction() {
final Map<Long, String> examNameMapping = this.resourceService.getExamNameMapping();
return event -> examNameMapping.get(event.examId);
}
private String getEventTime(final ExtendedClientEvent event) {
if (event == null || event.serverTime == null) {
return Constants.EMPTY_NOTE;
}
return this.i18nSupport
.formatDisplayDateTime(Utils.toDateTimeUTC(event.serverTime));
}
}

View file

@ -1,194 +1,194 @@
/*
* 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 java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.swt.widgets.Composite;
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.sebconfig.ConfigCreationInfo;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType;
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.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
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.seb.examconfig.CopyConfiguration;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.NewExamConfig;
final class SebExamConfigCreationPopup {
static final LocTextKey FORM_COPY_TEXT_KEY =
new LocTextKey("sebserver.examconfig.action.copy.dialog");
static final LocTextKey FORM_COPY_AS_TEMPLATE_TEXT_KEY =
new LocTextKey("sebserver.examconfig.action.copy-as-template.dialog");
static final LocTextKey FORM_CREATE_FROM_TEMPLATE_TEXT_KEY =
new LocTextKey("sebserver.configtemplate.action.create-config.dialog");
static Function<PageAction, PageAction> configCreationFunction(
final PageService pageService,
final PageContext pageContext) {
final boolean copyAsTemplate = BooleanUtils.toBoolean(
pageContext.getAttribute(PageContext.AttributeKeys.COPY_AS_TEMPLATE));
final boolean createFromTemplate = BooleanUtils.toBoolean(
pageContext.getAttribute(PageContext.AttributeKeys.CREATE_FROM_TEMPLATE));
return action -> {
final ModalInputDialog<FormHandle<ConfigCreationInfo>> dialog =
new ModalInputDialog<FormHandle<ConfigCreationInfo>>(
action.pageContext().getParent().getShell(),
pageService.getWidgetFactory())
.setLargeDialogWidth();
final CreationFormContext formContext = new CreationFormContext(
pageService,
pageContext,
copyAsTemplate,
createFromTemplate);
final Predicate<FormHandle<ConfigCreationInfo>> doCopy = formHandle -> doCreate(
pageService,
pageContext,
copyAsTemplate,
createFromTemplate,
formHandle);
final LocTextKey title = (copyAsTemplate)
? FORM_COPY_AS_TEMPLATE_TEXT_KEY
: (createFromTemplate)
? FORM_CREATE_FROM_TEMPLATE_TEXT_KEY
: FORM_COPY_TEXT_KEY;
dialog.open(
title,
doCopy,
Utils.EMPTY_EXECUTION,
formContext);
return action;
};
}
private static final boolean doCreate(
final PageService pageService,
final PageContext pageContext,
final boolean copyAsTemplate,
final boolean createFromTemplate,
final FormHandle<ConfigCreationInfo> formHandle) {
// create either a new configuration form template or from other configuration
final Class<? extends RestCall<ConfigurationNode>> restCall = (createFromTemplate)
? NewExamConfig.class
: CopyConfiguration.class;
final ConfigurationNode newConfig = pageService
.getRestService()
.getBuilder(restCall)
.withFormBinding(formHandle.getFormBinding())
.call()
.onError(formHandle::handleError)
.getOr(null);
if (newConfig == null) {
return false;
}
// view either new template or configuration
final PageAction viewCopy = (copyAsTemplate)
? pageService.pageActionBuilder(pageContext)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_TEMPLATE_VIEW)
.withEntityKey(new EntityKey(newConfig.id, EntityType.CONFIGURATION_NODE))
.create()
: pageService.pageActionBuilder(pageContext)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)
.withEntityKey(new EntityKey(newConfig.id, EntityType.CONFIGURATION_NODE))
.create();
pageService.executePageAction(viewCopy);
return true;
}
private static final class CreationFormContext implements ModalInputDialogComposer<FormHandle<ConfigCreationInfo>> {
private final PageService pageService;
private final PageContext pageContext;
private final boolean copyAsTemplate;
private final boolean createFromTemplate;
protected CreationFormContext(
final PageService pageService,
final PageContext pageContext,
final boolean copyAsTemplate,
final boolean createFromTemplate) {
this.pageService = pageService;
this.pageContext = pageContext;
this.copyAsTemplate = copyAsTemplate;
this.createFromTemplate = createFromTemplate;
}
@Override
public Supplier<FormHandle<ConfigCreationInfo>> compose(final Composite parent) {
final Composite grid = this.pageService.getWidgetFactory()
.createPopupScrollComposite(parent);
final EntityKey entityKey = this.pageContext.getEntityKey();
final FormHandle<ConfigCreationInfo> formHandle = this.pageService.formBuilder(
this.pageContext.copyOf(grid))
.readonly(false)
.putStaticValueIf(
() -> !this.createFromTemplate,
Domain.CONFIGURATION_NODE.ATTR_ID,
entityKey.getModelId())
.putStaticValue(
Domain.CONFIGURATION_NODE.ATTR_TYPE,
(this.copyAsTemplate)
? ConfigurationType.TEMPLATE.name()
: ConfigurationType.EXAM_CONFIG.name())
.putStaticValueIf(
() -> this.createFromTemplate,
Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID,
entityKey.getModelId())
.addField(FormBuilder.text(
Domain.CONFIGURATION_NODE.ATTR_NAME,
SebExamConfigPropForm.FORM_NAME_TEXT_KEY))
.addField(FormBuilder.text(
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
SebExamConfigPropForm.FORM_DESCRIPTION_TEXT_KEY)
.asArea())
.addFieldIf(
() -> !this.copyAsTemplate && !this.createFromTemplate,
() -> FormBuilder.checkbox(
ConfigCreationInfo.ATTR_COPY_WITH_HISTORY,
SebExamConfigPropForm.FORM_HISTORY_TEXT_KEY))
.build();
return () -> formHandle;
}
}
}
/*
* 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 java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.swt.widgets.Composite;
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.sebconfig.ConfigCreationInfo;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType;
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.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
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.seb.examconfig.CopyConfiguration;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.NewExamConfig;
final class SebExamConfigCreationPopup {
static final LocTextKey FORM_COPY_TEXT_KEY =
new LocTextKey("sebserver.examconfig.action.copy.dialog");
static final LocTextKey FORM_COPY_AS_TEMPLATE_TEXT_KEY =
new LocTextKey("sebserver.examconfig.action.copy-as-template.dialog");
static final LocTextKey FORM_CREATE_FROM_TEMPLATE_TEXT_KEY =
new LocTextKey("sebserver.configtemplate.action.create-config.dialog");
static Function<PageAction, PageAction> configCreationFunction(
final PageService pageService,
final PageContext pageContext) {
final boolean copyAsTemplate = BooleanUtils.toBoolean(
pageContext.getAttribute(PageContext.AttributeKeys.COPY_AS_TEMPLATE));
final boolean createFromTemplate = BooleanUtils.toBoolean(
pageContext.getAttribute(PageContext.AttributeKeys.CREATE_FROM_TEMPLATE));
return action -> {
final ModalInputDialog<FormHandle<ConfigCreationInfo>> dialog =
new ModalInputDialog<FormHandle<ConfigCreationInfo>>(
action.pageContext().getParent().getShell(),
pageService.getWidgetFactory())
.setLargeDialogWidth();
final CreationFormContext formContext = new CreationFormContext(
pageService,
pageContext,
copyAsTemplate,
createFromTemplate);
final Predicate<FormHandle<ConfigCreationInfo>> doCopy = formHandle -> doCreate(
pageService,
pageContext,
copyAsTemplate,
createFromTemplate,
formHandle);
final LocTextKey title = (copyAsTemplate)
? FORM_COPY_AS_TEMPLATE_TEXT_KEY
: (createFromTemplate)
? FORM_CREATE_FROM_TEMPLATE_TEXT_KEY
: FORM_COPY_TEXT_KEY;
dialog.open(
title,
doCopy,
Utils.EMPTY_EXECUTION,
formContext);
return action;
};
}
private static boolean doCreate(
final PageService pageService,
final PageContext pageContext,
final boolean copyAsTemplate,
final boolean createFromTemplate,
final FormHandle<ConfigCreationInfo> formHandle) {
// create either a new configuration form template or from other configuration
final Class<? extends RestCall<ConfigurationNode>> restCall = (createFromTemplate)
? NewExamConfig.class
: CopyConfiguration.class;
final ConfigurationNode newConfig = pageService
.getRestService()
.getBuilder(restCall)
.withFormBinding(formHandle.getFormBinding())
.call()
.onError(formHandle::handleError)
.getOr(null);
if (newConfig == null) {
return false;
}
// view either new template or configuration
final PageAction viewCopy = (copyAsTemplate)
? pageService.pageActionBuilder(pageContext)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_TEMPLATE_VIEW)
.withEntityKey(new EntityKey(newConfig.id, EntityType.CONFIGURATION_NODE))
.create()
: pageService.pageActionBuilder(pageContext)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)
.withEntityKey(new EntityKey(newConfig.id, EntityType.CONFIGURATION_NODE))
.create();
pageService.executePageAction(viewCopy);
return true;
}
private static final class CreationFormContext implements ModalInputDialogComposer<FormHandle<ConfigCreationInfo>> {
private final PageService pageService;
private final PageContext pageContext;
private final boolean copyAsTemplate;
private final boolean createFromTemplate;
protected CreationFormContext(
final PageService pageService,
final PageContext pageContext,
final boolean copyAsTemplate,
final boolean createFromTemplate) {
this.pageService = pageService;
this.pageContext = pageContext;
this.copyAsTemplate = copyAsTemplate;
this.createFromTemplate = createFromTemplate;
}
@Override
public Supplier<FormHandle<ConfigCreationInfo>> compose(final Composite parent) {
final Composite grid = this.pageService.getWidgetFactory()
.createPopupScrollComposite(parent);
final EntityKey entityKey = this.pageContext.getEntityKey();
final FormHandle<ConfigCreationInfo> formHandle = this.pageService.formBuilder(
this.pageContext.copyOf(grid))
.readonly(false)
.putStaticValueIf(
() -> !this.createFromTemplate,
Domain.CONFIGURATION_NODE.ATTR_ID,
entityKey.getModelId())
.putStaticValue(
Domain.CONFIGURATION_NODE.ATTR_TYPE,
(this.copyAsTemplate)
? ConfigurationType.TEMPLATE.name()
: ConfigurationType.EXAM_CONFIG.name())
.putStaticValueIf(
() -> this.createFromTemplate,
Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID,
entityKey.getModelId())
.addField(FormBuilder.text(
Domain.CONFIGURATION_NODE.ATTR_NAME,
SebExamConfigPropForm.FORM_NAME_TEXT_KEY))
.addField(FormBuilder.text(
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
SebExamConfigPropForm.FORM_DESCRIPTION_TEXT_KEY)
.asArea())
.addFieldIf(
() -> !this.copyAsTemplate && !this.createFromTemplate,
() -> FormBuilder.checkbox(
ConfigCreationInfo.ATTR_COPY_WITH_HISTORY,
SebExamConfigPropForm.FORM_HISTORY_TEXT_KEY))
.build();
return () -> formHandle;
}
}
}

View file

@ -183,6 +183,11 @@ public enum ActionDefinition {
ImageIcon.SAVE,
PageStateDefinitionImpl.LMS_SETUP_VIEW,
ActionCategory.FORM),
LMS_SETUP_SAVE_AND_ACTIVATE(
new LocTextKey("sebserver.form.action.save.activate"),
ImageIcon.ACTIVE,
PageStateDefinitionImpl.LMS_SETUP_VIEW,
ActionCategory.FORM),
LMS_SETUP_ACTIVATE(
new LocTextKey("sebserver.lmssetup.action.activate"),
ImageIcon.TOGGLE_OFF,
@ -193,6 +198,11 @@ public enum ActionDefinition {
ImageIcon.TOGGLE_ON,
PageStateDefinitionImpl.LMS_SETUP_VIEW,
ActionCategory.FORM),
LMS_SETUP_TOGGLE_ACTIVITY(
new LocTextKey("sebserver.overall.action.toggle-activity"),
ImageIcon.SWITCH,
PageStateDefinitionImpl.LMS_SETUP_LIST,
ActionCategory.LMS_SETUP_LIST),
QUIZ_DISCOVERY_VIEW_LIST(
new LocTextKey("sebserver.quizdiscovery.action.list"),
@ -593,7 +603,7 @@ public enum ActionDefinition {
ImageIcon.SEND_QUIT,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FORM),
MONITOR_EXAM_FROM_DETAILS(
MONITOR_EXAM_BACK_TO_OVERVIEW(
new LocTextKey("sebserver.monitoring.exam.action.detail.view"),
ImageIcon.SHOW,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,

View file

@ -1,185 +1,185 @@
/*
* Copyright (c) 2018 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.form;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.form.Form.FormFieldAccessor;
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.FieldValidationError;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.FormBinding;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall.CallType;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
public class FormHandle<T extends Entity> {
private static final Logger log = LoggerFactory.getLogger(FormHandle.class);
public static final String FIELD_VALIDATION_LOCTEXT_PREFIX = "sebserver.form.validation.fieldError.";
private final PageService pageService;
private final PageContext pageContext;
private final Form form;
private final RestCall<T> post;
private final I18nSupport i18nSupport;
FormHandle(
final PageService pageService,
final PageContext pageContext,
final Form form,
final RestCall<T> post) {
this.pageService = pageService;
this.pageContext = pageContext;
this.form = form;
this.post = post;
this.i18nSupport = pageService.getI18nSupport();
}
public PageContext getContext() {
return this.pageContext;
}
public FormBinding getFormBinding() {
return this.form;
}
public Form getForm() {
return this.form;
}
/** Process an API post request to send and save the form field values
* to the webservice and publishes a page event to return to read-only-view
* to indicate that the data was successfully saved or process an validation
* error indication if there are some validation errors.
*
* @param action the save action context
* @return the new Action context for read-only-view */
public final PageAction processFormSave(final PageAction action) {
return handleFormPost(doAPIPost(), action);
}
public final PageAction saveAndActivate(final PageAction action) {
final PageAction handleFormPost = handleFormPost(doAPIPost(), action);
final EntityType entityType = this.post.getEntityType();
this.pageService.getRestService().getBuilder(entityType, CallType.ACTIVATION_ACTIVATE)
.withURIVariable(API.PARAM_MODEL_ID, handleFormPost.getEntityKey().getModelId())
.call()
.getOrThrow();
return handleFormPost;
}
/** process a form post by first resetting all field validation errors (if there are some)
* then collecting all input data from the form by form-binding to a either a JSON string in
* HTTP PUT case or to an form-URL-encoded string on HTTP POST case. And PUT or POST the data
* to the webservice by using the defined RestCall and return the response result of the RestCall.
*
* @return the response result of the post (or put) RestCall */
public Result<T> doAPIPost() {
// reset all errors that may still be displayed
this.form.process(
Utils.truePredicate(),
fieldAccessor -> fieldAccessor.resetError());
// post
return this.post
.newBuilder()
.withFormBinding(this.form)
.call();
}
/** Uses the result of a form post to either create and publish a new Action to
* go to the read-only-view of the specified form to indicate a successful form post
* or stay within the edit-mode of the form and indicate errors or field validation messages
* to the user on error case.
*
* @param postResult The form post result
* @param action the action that was applied with the form post
* @return the new Action that was used to stay on page or go the read-only-view of the form */
public PageAction handleFormPost(final Result<T> postResult, final PageAction action) {
return postResult
.map(result -> {
PageAction resultAction = this.pageService.pageActionBuilder(action.pageContext())
.newAction(action.definition)
.create();
if (resultAction.getEntityKey() == null) {
resultAction = resultAction.withEntityKey(result.getEntityKey());
}
return resultAction;
})
.onError(this::handleError)
.getOrThrow(error -> new FormPostException(error));
}
public boolean handleError(final Exception error) {
if (error instanceof RestCallError) {
((RestCallError) error)
.getErrorMessages()
.stream()
.filter(APIMessage.ErrorMessage.FIELD_VALIDATION::isOf)
.map(FieldValidationError::new)
.forEach(fve -> this.form.process(
name -> name.equals(fve.fieldName),
fieldAccessor -> showValidationError(fieldAccessor, fve)));
return true;
} else {
log.error("Unexpected error while trying to post form: {}", error.getMessage());
final EntityType resultType = this.post.getEntityType();
if (resultType != null) {
this.pageContext.notifySaveError(resultType, error);
} else {
this.pageContext.notifyError(
new LocTextKey(PageContext.GENERIC_SAVE_ERROR_TEXT_KEY, Constants.EMPTY_NOTE),
error);
}
return false;
}
}
public boolean hasAnyError() {
return this.form.hasAnyError();
}
private final void showValidationError(
final FormFieldAccessor fieldAccessor,
final FieldValidationError valError) {
fieldAccessor.setError(this.i18nSupport.getText(new LocTextKey(
FIELD_VALIDATION_LOCTEXT_PREFIX + valError.errorType,
(Object[]) valError.getAttributes())));
}
public FormHandle<T> process(
final Predicate<String> nameFilter,
final Consumer<FormFieldAccessor> processor) {
this.form.process(nameFilter, processor);
return this;
}
}
/*
* Copyright (c) 2018 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.form;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.form.Form.FormFieldAccessor;
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.FieldValidationError;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.FormBinding;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall.CallType;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
public class FormHandle<T extends Entity> {
private static final Logger log = LoggerFactory.getLogger(FormHandle.class);
public static final String FIELD_VALIDATION_LOCTEXT_PREFIX = "sebserver.form.validation.fieldError.";
private final PageService pageService;
private final PageContext pageContext;
private final Form form;
private final RestCall<T> post;
private final I18nSupport i18nSupport;
FormHandle(
final PageService pageService,
final PageContext pageContext,
final Form form,
final RestCall<T> post) {
this.pageService = pageService;
this.pageContext = pageContext;
this.form = form;
this.post = post;
this.i18nSupport = pageService.getI18nSupport();
}
public PageContext getContext() {
return this.pageContext;
}
public FormBinding getFormBinding() {
return this.form;
}
public Form getForm() {
return this.form;
}
/** Process an API post request to send and save the form field values
* to the webservice and publishes a page event to return to read-only-view
* to indicate that the data was successfully saved or process an validation
* error indication if there are some validation errors.
*
* @param action the save action context
* @return the new Action context for read-only-view */
public final PageAction processFormSave(final PageAction action) {
return handleFormPost(doAPIPost(), action);
}
public final PageAction saveAndActivate(final PageAction action) {
final PageAction handleFormPost = handleFormPost(doAPIPost(), action);
final EntityType entityType = this.post.getEntityType();
this.pageService.getRestService().getBuilder(entityType, CallType.ACTIVATION_ACTIVATE)
.withURIVariable(API.PARAM_MODEL_ID, handleFormPost.getEntityKey().getModelId())
.call()
.getOrThrow();
return handleFormPost;
}
/** process a form post by first resetting all field validation errors (if there are some)
* then collecting all input data from the form by form-binding to a either a JSON string in
* HTTP PUT case or to an form-URL-encoded string on HTTP POST case. And PUT or POST the data
* to the webservice by using the defined RestCall and return the response result of the RestCall.
*
* @return the response result of the post (or put) RestCall */
public Result<T> doAPIPost() {
// reset all errors that may still be displayed
this.form.process(
Utils.truePredicate(),
fieldAccessor -> fieldAccessor.resetError());
// post
return this.post
.newBuilder()
.withFormBinding(this.form)
.call();
}
/** Uses the result of a form post to either create and publish a new Action to
* go to the read-only-view of the specified form to indicate a successful form post
* or stay within the edit-mode of the form and indicate errors or field validation messages
* to the user on error case.
*
* @param postResult The form post result
* @param action the action that was applied with the form post
* @return the new Action that was used to stay on page or go the read-only-view of the form */
public PageAction handleFormPost(final Result<T> postResult, final PageAction action) {
return postResult
.map(result -> {
PageAction resultAction = this.pageService.pageActionBuilder(action.pageContext())
.newAction(action.definition)
.create();
if (resultAction.getEntityKey() == null) {
resultAction = resultAction.withEntityKey(result.getEntityKey());
}
return resultAction;
})
.onError(this::handleError)
.getOrThrow(FormPostException::new);
}
public boolean handleError(final Exception error) {
if (error instanceof RestCallError) {
((RestCallError) error)
.getErrorMessages()
.stream()
.filter(APIMessage.ErrorMessage.FIELD_VALIDATION::isOf)
.map(FieldValidationError::new)
.forEach(fve -> this.form.process(
name -> name.equals(fve.fieldName),
fieldAccessor -> showValidationError(fieldAccessor, fve)));
return true;
} else {
log.error("Unexpected error while trying to post form: {}", error.getMessage());
final EntityType resultType = this.post.getEntityType();
if (resultType != null) {
this.pageContext.notifySaveError(resultType, error);
} else {
this.pageContext.notifyError(
new LocTextKey(PageContext.GENERIC_SAVE_ERROR_TEXT_KEY, Constants.EMPTY_NOTE),
error);
}
return false;
}
}
public boolean hasAnyError() {
return this.form.hasAnyError();
}
private final void showValidationError(
final FormFieldAccessor fieldAccessor,
final FieldValidationError valError) {
fieldAccessor.setError(this.i18nSupport.getText(new LocTextKey(
FIELD_VALIDATION_LOCTEXT_PREFIX + valError.errorType,
(Object[]) valError.getAttributes())));
}
public FormHandle<T> process(
final Predicate<String> nameFilter,
final Consumer<FormFieldAccessor> processor) {
this.form.process(nameFilter, processor);
return this;
}
}

View file

@ -1,150 +1,150 @@
/*
* 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.form;
import java.util.function.Consumer;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
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;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
public final class TextFieldBuilder extends FieldBuilder<String> {
private static final String HTML_TEXT_BLOCK_START =
"<span style=\"font: 12px Arial, Helvetica, sans-serif;color: #4a4a4a;\">";
private static final String HTML_TEXT_BLOCK_END = "</span>";
boolean isPassword = false;
boolean isNumber = false;
Consumer<String> numberCheck = null;
boolean isArea = false;
int areaMinHeight = WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT;
boolean isColorbox = false;
boolean isHTML = false;
TextFieldBuilder(final String name, final LocTextKey label, final String value) {
super(name, label, value);
}
public TextFieldBuilder asPasswordField() {
this.isPassword = true;
return this;
}
public TextFieldBuilder asNumber() {
this.isNumber = true;
return this;
}
public TextFieldBuilder asNumber(final Consumer<String> numberCheck) {
this.isNumber = true;
this.numberCheck = numberCheck;
return this;
}
public TextFieldBuilder asArea(final int minHeight) {
this.areaMinHeight = minHeight;
return asArea();
}
public TextFieldBuilder asArea() {
this.isArea = true;
this.titleValign = SWT.CENTER;
return this;
}
public TextFieldBuilder asHTML() {
this.isHTML = true;
return this;
}
public FieldBuilder<?> asHTML(final boolean html) {
this.isHTML = html;
return this;
}
public TextFieldBuilder asColorbox() {
this.isColorbox = true;
return this;
}
@Override
void build(final FormBuilder builder) {
final boolean readonly = builder.readonly || this.readonly;
final Label titleLabel = createTitleLabel(builder.formParent, builder, this);
final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput);
if (readonly && this.isHTML) {
final Browser browser = new Browser(fieldGrid, SWT.NONE);
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
gridData.minimumHeight = this.areaMinHeight;
browser.setBackground(new Color(builder.formParent.getDisplay(), new RGB(250, 250, 250)));
browser.setLayoutData(gridData);
if (StringUtils.isNoneBlank(this.value)) {
browser.setText(createHTMLText(this.value));
} else if (readonly) {
browser.setText(Constants.EMPTY_NOTE);
}
builder.form.putReadonlyField(this.name, titleLabel, browser);
return;
}
final Text textInput = (this.isNumber)
? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly)
: (this.isArea)
? builder.widgetFactory.textAreaInput(fieldGrid, readonly)
: builder.widgetFactory.textInput(fieldGrid, this.isPassword, readonly);
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
if (this.isArea) {
gridData.minimumHeight = this.areaMinHeight;
} else if (this.isColorbox) {
gridData.minimumHeight = WidgetFactory.TEXT_INPUT_MIN_HEIGHT;
textInput.setData(RWT.CUSTOM_VARIANT, "colorbox");
}
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, titleLabel, textInput);
} else {
final Label errorLabel = createErrorLabel(fieldGrid);
builder.form.putField(this.name, titleLabel, textInput, errorLabel);
builder.setFieldVisible(this.visible, this.name);
}
}
private String createHTMLText(final String text) {
return HTML_TEXT_BLOCK_START
+ text
.replace("<a", "<span")
.replace("</a", "</span")
.replace("<A", "<span")
.replace("</A", "</span")
+ HTML_TEXT_BLOCK_END;
}
/*
* 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.form;
import java.util.function.Consumer;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
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;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
public final class TextFieldBuilder extends FieldBuilder<String> {
private static final String HTML_TEXT_BLOCK_START =
"<span style=\"font: 12px Arial, Helvetica, sans-serif;color: #4a4a4a;\">";
private static final String HTML_TEXT_BLOCK_END = "</span>";
boolean isPassword = false;
boolean isNumber = false;
Consumer<String> numberCheck = null;
boolean isArea = false;
int areaMinHeight = WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT;
boolean isColorBox = false;
boolean isHTML = false;
TextFieldBuilder(final String name, final LocTextKey label, final String value) {
super(name, label, value);
}
public TextFieldBuilder asPasswordField() {
this.isPassword = true;
return this;
}
public TextFieldBuilder asNumber() {
this.isNumber = true;
return this;
}
public TextFieldBuilder asNumber(final Consumer<String> numberCheck) {
this.isNumber = true;
this.numberCheck = numberCheck;
return this;
}
public TextFieldBuilder asArea(final int minHeight) {
this.areaMinHeight = minHeight;
return asArea();
}
public TextFieldBuilder asArea() {
this.isArea = true;
this.titleValign = SWT.CENTER;
return this;
}
public TextFieldBuilder asHTML() {
this.isHTML = true;
return this;
}
public FieldBuilder<?> asHTML(final boolean html) {
this.isHTML = html;
return this;
}
public TextFieldBuilder asColorBox() {
this.isColorBox = true;
return this;
}
@Override
void build(final FormBuilder builder) {
final boolean readonly = builder.readonly || this.readonly;
final Label titleLabel = createTitleLabel(builder.formParent, builder, this);
final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput);
if (readonly && this.isHTML) {
final Browser browser = new Browser(fieldGrid, SWT.NONE);
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
gridData.minimumHeight = this.areaMinHeight;
browser.setBackground(new Color(builder.formParent.getDisplay(), new RGB(250, 250, 250)));
browser.setLayoutData(gridData);
if (StringUtils.isNoneBlank(this.value)) {
browser.setText(createHTMLText(this.value));
} else if (readonly) {
browser.setText(Constants.EMPTY_NOTE);
}
builder.form.putReadonlyField(this.name, titleLabel, browser);
return;
}
final Text textInput = (this.isNumber)
? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly)
: (this.isArea)
? builder.widgetFactory.textAreaInput(fieldGrid, readonly)
: builder.widgetFactory.textInput(fieldGrid, this.isPassword, readonly);
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
if (this.isArea) {
gridData.minimumHeight = this.areaMinHeight;
} else if (this.isColorBox) {
gridData.minimumHeight = WidgetFactory.TEXT_INPUT_MIN_HEIGHT;
textInput.setData(RWT.CUSTOM_VARIANT, "colorbox");
}
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, titleLabel, textInput);
} else {
final Label errorLabel = createErrorLabel(fieldGrid);
builder.form.putField(this.name, titleLabel, textInput, errorLabel);
builder.setFieldVisible(this.visible, this.name);
}
}
private String createHTMLText(final String text) {
return HTML_TEXT_BLOCK_START
+ text
.replace("<a", "<span")
.replace("</a", "</span")
.replace("<A", "<span")
.replace("</A", "</span")
+ HTML_TEXT_BLOCK_END;
}
}

View file

@ -164,8 +164,7 @@ public class ResourceService {
}
public List<Tuple<String>> lmsTypeResources() {
return Arrays.asList(LmsType.values())
.stream()
return Arrays.stream(LmsType.values())
.filter(lmsType -> lmsType != LmsType.MOCKUP || this.mock_lms_enabled)
.map(lmsType -> new Tuple<>(
lmsType.name(),
@ -175,8 +174,7 @@ public class ResourceService {
}
public List<Tuple<String>> clientEventTypeResources() {
return Arrays.asList(EventType.values())
.stream()
return Arrays.stream(EventType.values())
.filter(eventType -> !CLIENT_EVENT_TYPE_EXCLUDE_MAP.contains(eventType))
.map(eventType -> new Tuple<>(
eventType.name(),
@ -200,8 +198,7 @@ public class ResourceService {
}
public List<Tuple<String>> indicatorTypeResources() {
return Arrays.asList(IndicatorType.values())
.stream()
return Arrays.stream(IndicatorType.values())
.map(type -> new Tuple3<>(
type.name(),
this.i18nSupport.getText(EXAM_INDICATOR_TYPE_PREFIX + type.name(), type.name()),
@ -295,8 +292,7 @@ public class ResourceService {
}
public List<Tuple<String>> entityTypeResources() {
return Arrays.asList(EntityType.values())
.stream()
return Arrays.stream(EntityType.values())
.filter(type -> !ENTITY_TYPE_EXCLUDE_MAP.contains(type))
.map(type -> new Tuple<>(type.name(), getEntityTypeName(type)))
.sorted(RESOURCE_COMPARATOR)
@ -319,8 +315,7 @@ public class ResourceService {
}
public List<Tuple<String>> userActivityTypeResources() {
return Arrays.asList(UserLogActivityType.values())
.stream()
return Arrays.stream(UserLogActivityType.values())
.map(type -> new Tuple<>(type.name(), getUserActivityTypeName(type)))
.sorted(RESOURCE_COMPARATOR)
.collect(Collectors.toList());
@ -363,16 +358,13 @@ public class ResourceService {
}
public List<Tuple<String>> examTypeResources() {
return Arrays.asList(ExamType.values())
.stream()
.filter(type -> type != ExamType.UNDEFINED)
return Arrays.stream(ExamType.values())
.map(type -> new Tuple3<>(
type.name(),
this.i18nSupport.getText(EXAM_TYPE_PREFIX + type.name()),
Utils.formatLineBreaks(this.i18nSupport.getText(
this.i18nSupport.getText(EXAM_TYPE_PREFIX + type.name()) + Constants.TOOLTIP_TEXT_KEY_SUFFIX,
StringUtils.EMPTY))
))
EXAM_INDICATOR_TYPE_PREFIX + type.name() + Constants.TOOLTIP_TEXT_KEY_SUFFIX,
StringUtils.EMPTY))))
.sorted(RESOURCE_COMPARATOR)
.collect(Collectors.toList());
}
@ -382,8 +374,7 @@ public class ResourceService {
}
public List<Tuple<String>> examConfigStatusResources(final boolean isAttachedToExam) {
return Arrays.asList(ConfigurationStatus.values())
.stream()
return Arrays.stream(ConfigurationStatus.values())
.filter(status -> {
if (isAttachedToExam) {
return status != ConfigurationStatus.READY_TO_USE;
@ -579,8 +570,7 @@ public class ResourceService {
}
public List<Tuple<String>> getAttributeTypeResources() {
return Arrays.asList(AttributeType.values())
.stream()
return Arrays.stream(AttributeType.values())
.filter(type -> !ATTRIBUTE_TYPES_NOT_DISPLAYED.contains(type))
.map(type -> new Tuple<>(getAttributeTypeFilterName(type), getAttributeTypeName(type)))
.sorted(RESOURCE_COMPARATOR)
@ -629,21 +619,25 @@ public class ResourceService {
}
public List<Tuple<String>> sebRestrictionWhiteListResources() {
return Arrays.asList(WhiteListPath.values())
.stream()
.map(type -> new Tuple<>(
return Arrays.stream(WhiteListPath.values())
.map(type -> new Tuple3<>(
type.key,
this.i18nSupport.getText(SEB_RESTRICTION_WHITE_LIST_PREFIX + type.name(), type.key)))
this.i18nSupport.getText(SEB_RESTRICTION_WHITE_LIST_PREFIX + type.name(), type.key),
Utils.formatLineBreaks(this.i18nSupport.getText(
SEB_RESTRICTION_WHITE_LIST_PREFIX + type.name() + Constants.TOOLTIP_TEXT_KEY_SUFFIX,
StringUtils.EMPTY))))
.sorted(RESOURCE_COMPARATOR)
.collect(Collectors.toList());
}
public List<Tuple<String>> sebRestrictionPermissionResources() {
return Arrays.asList(PermissionComponent.values())
.stream()
.map(type -> new Tuple<>(
return Arrays.stream(PermissionComponent.values())
.map(type -> new Tuple3<>(
type.key,
this.i18nSupport.getText(SEB_RESTRICTION_PERMISSIONS_PREFIX + type.name(), type.key)))
this.i18nSupport.getText(SEB_RESTRICTION_PERMISSIONS_PREFIX + type.name(), type.key),
Utils.formatLineBreaks(this.i18nSupport.getText(
SEB_RESTRICTION_PERMISSIONS_PREFIX + type.name() + Constants.TOOLTIP_TEXT_KEY_SUFFIX,
StringUtils.EMPTY))))
.sorted(RESOURCE_COMPARATOR)
.collect(Collectors.toList());
}

View file

@ -1,384 +1,396 @@
/*
* 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.page.impl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.servlet.http.HttpSession;
import org.eclipse.rap.rwt.RWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.Activatable;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
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.i18n.PolyglotPageService;
import ch.ethz.seb.sebserver.gui.service.page.ComposerService;
import ch.ethz.seb.sebserver.gui.service.page.MultiPageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.PageStateDefinition.Type;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.PageEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.PageEventListener;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall.CallType;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.table.TableBuilder;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Lazy
@Service
@GuiProfile
public class PageServiceImpl implements PageService {
private static final LocTextKey CONFIRM_DEACTIVATION_NO_DEP_KEY =
new LocTextKey("sebserver.dialog.confirm.deactivation.noDependencies");
private static final String CONFIRM_DEACTIVATION_KEY = "sebserver.dialog.confirm.deactivation";
private static final Logger log = LoggerFactory.getLogger(PageServiceImpl.class);
private static final LocTextKey MSG_GO_AWAY_FROM_EDIT =
new LocTextKey("sebserver.overall.action.goAwayFromEditPageConfirm");
private static final String ATTR_PAGE_STATE = "PAGE_STATE";
private static final ListenerComparator LIST_COMPARATOR = new ListenerComparator();
private final JSONMapper jsonMapper;
private final WidgetFactory widgetFactory;
private final PolyglotPageService polyglotPageService;
private final ResourceService resourceService;
private final CurrentUser currentUser;
public PageServiceImpl(
final JSONMapper jsonMapper,
final WidgetFactory widgetFactory,
final PolyglotPageService polyglotPageService,
final ResourceService resourceService,
final CurrentUser currentUser) {
this.jsonMapper = jsonMapper;
this.widgetFactory = widgetFactory;
this.polyglotPageService = polyglotPageService;
this.resourceService = resourceService;
this.currentUser = currentUser;
}
@Override
public WidgetFactory getWidgetFactory() {
return this.widgetFactory;
}
@Override
public PolyglotPageService getPolyglotPageService() {
return this.polyglotPageService;
}
@Override
public AuthorizationContextHolder getAuthorizationContextHolder() {
return this.currentUser.getAuthorizationContextHolder();
}
@Override
public I18nSupport getI18nSupport() {
return this.widgetFactory.getI18nSupport();
}
@Override
public ResourceService getResourceService() {
return this.resourceService;
}
@Override
public JSONMapper getJSONMapper() {
return this.jsonMapper;
}
@Override
public RestService getRestService() {
if (this.resourceService == null) {
return null;
}
return this.resourceService.getRestService();
}
@Override
public CurrentUser getCurrentUser() {
return this.currentUser;
}
@Override
public PageState getCurrentState() {
try {
final HttpSession httpSession = RWT
.getUISession()
.getHttpSession();
return (PageState) httpSession.getAttribute(ATTR_PAGE_STATE);
} catch (final Exception e) {
log.error("Failed to get current PageState: ", e);
return null;
}
}
@Override
@SuppressWarnings("unchecked")
public <T extends PageEvent> void firePageEvent(final T event, final PageContext pageContext) {
final Class<? extends PageEvent> typeClass = event.getClass();
final List<PageEventListener<T>> listeners = new ArrayList<>();
ComposerService.traversePageTree(
pageContext.getRoot(),
c -> {
final PageEventListener<?> listener =
(PageEventListener<?>) c.getData(PageEventListener.LISTENER_ATTRIBUTE_KEY);
return listener != null && listener.match(typeClass);
},
c -> listeners.add(((PageEventListener<T>) c.getData(PageEventListener.LISTENER_ATTRIBUTE_KEY))));
if (listeners.isEmpty()) {
return;
}
listeners.stream()
.sorted(LIST_COMPARATOR)
.forEach(listener -> {
try {
listener.notify(event);
} catch (final Exception e) {
log.error("Unexpected error while notify PageEventListener: ", e);
}
});
}
@Override
public void executePageAction(final PageAction pageAction, final Consumer<Result<PageAction>> callback) {
final PageState currentState = getCurrentState();
if (!pageAction.ignoreMoveAwayFromEdit && currentState != null && currentState.type() == Type.FORM_EDIT) {
pageAction.pageContext().applyConfirmDialog(
MSG_GO_AWAY_FROM_EDIT,
confirm -> {
if (confirm) {
exec(pageAction, callback);
} else {
callback.accept(Result.ofRuntimeError("Confirm denied"));
}
});
} else {
exec(pageAction, callback);
}
}
@Override
public <T extends Entity & Activatable> Supplier<LocTextKey> confirmDeactivation(final Set<? extends T> entities) {
final RestService restService = this.resourceService.getRestService();
return () -> {
if (entities == null || entities.isEmpty()) {
return null;
}
try {
final int dependencies = entities.stream()
.flatMap(entity -> {
final RestCall<Set<EntityKey>>.RestCallBuilder builder =
restService.<Set<EntityKey>> getBuilder(
entity.entityType(),
CallType.GET_DEPENDENCIES);
return builder
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(entity.getModelId()))
.withQueryParam(API.PARAM_BULK_ACTION_TYPE, BulkActionType.DEACTIVATE.name())
.call()
.getOrThrow().stream();
})
.collect(Collectors.toList())
.size();
if (dependencies > 0) {
return new LocTextKey(CONFIRM_DEACTIVATION_KEY, String.valueOf(dependencies));
} else {
return CONFIRM_DEACTIVATION_NO_DEP_KEY;
}
} catch (final Exception e) {
log.warn("Failed to get dependencyies. Error: {}", e.getMessage());
return new LocTextKey(CONFIRM_DEACTIVATION_KEY, "");
}
};
}
@Override
public <T extends Entity & Activatable> Function<PageAction, PageAction> activationToggleActionFunction(
final EntityTable<T> table,
final LocTextKey noSelectionText) {
return action -> {
final Set<T> selectedROWData = table.getSelectedROWData();
if (selectedROWData == null || selectedROWData.isEmpty()) {
throw new PageMessageException(noSelectionText);
}
final RestService restService = this.resourceService.getRestService();
final EntityType entityType = table.getEntityType();
final Collection<Exception> errors = new ArrayList<>();
for (final T entity : selectedROWData) {
if (entity.isActive()) {
restService.getBuilder(entityType, CallType.ACTIVATION_DEACTIVATE)
.withURIVariable(API.PARAM_MODEL_ID, entity.getModelId())
.call()
.onError(errors::add);
} else {
restService.getBuilder(entityType, CallType.ACTIVATION_ACTIVATE)
.withURIVariable(API.PARAM_MODEL_ID, entity.getModelId())
.call()
.onError(errors::add);
}
}
if (!errors.isEmpty()) {
final String entityTypeName = this.resourceService.getEntityTypeName(entityType);
throw new MultiPageMessageException(
new LocTextKey(PageContext.GENERIC_ACTIVATE_ERROR_TEXT_KEY, entityTypeName),
errors);
}
return action;
};
}
private void exec(final PageAction pageAction, final Consumer<Result<PageAction>> callback) {
pageAction.applyAction(result -> {
if (!result.hasError()) {
final PageAction action = result.get();
if (pageAction.fireActionEvent) {
firePageEvent(new ActionEvent(action), action.pageContext());
}
try {
final HttpSession httpSession = RWT
.getUISession()
.getHttpSession();
if (action != null &&
action.fireActionEvent &&
action.definition != null &&
action.definition.targetState != null) {
final PageState pageState = new PageState(action.definition.targetState, action);
if (log.isDebugEnabled()) {
log.debug("Set session PageState: {} : {}", pageState, httpSession.getId());
}
httpSession.setAttribute(ATTR_PAGE_STATE, pageState);
}
} catch (final Exception e) {
log.error("Failed to set current PageState: ", e);
}
}
callback.accept(result);
});
}
@Override
public void publishAction(final PageAction pageAction, final boolean active) {
this.firePageEvent(new ActionPublishEvent(pageAction, active), pageAction.pageContext());
}
@Override
public FormBuilder formBuilder(final PageContext pageContext, final int rows) {
return new FormBuilder(this, pageContext, rows);
}
@Override
public <T extends Entity> TableBuilder<T> entityTableBuilder(
final String name,
final RestCall<Page<T>> apiCall) {
return new TableBuilder<>(name, this, apiCall);
}
@Override
public void logout(final PageContext pageContext) {
this.clearState();
try {
final boolean logoutSuccessful = this.currentUser.logout();
if (!logoutSuccessful) {
log.warn("Failed to logout. See logfiles for more information");
}
} catch (final Exception e) {
log.info("Cleanup logout failed: {}", e.getMessage());
} finally {
pageContext.forwardToLoginPage();
}
}
@Override
public void clearState() {
try {
final HttpSession httpSession = RWT
.getUISession()
.getHttpSession();
log.debug("Clear session PageState: {}", httpSession.getId());
httpSession.removeAttribute(ATTR_PAGE_STATE);
} catch (final Exception e) {
log.error("Failed to clear current PageState: ", e);
}
}
private static final class ListenerComparator implements Comparator<PageEventListener<?>>, Serializable {
private static final long serialVersionUID = 2571739214439340404L;
@Override
public int compare(final PageEventListener<?> o1, final PageEventListener<?> o2) {
final int x = o1.priority();
final int y = o2.priority();
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
}
}
/*
* 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.page.impl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.servlet.http.HttpSession;
import org.eclipse.rap.rwt.RWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.Activatable;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
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.i18n.PolyglotPageService;
import ch.ethz.seb.sebserver.gui.service.page.ComposerService;
import ch.ethz.seb.sebserver.gui.service.page.MultiPageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.PageStateDefinition.Type;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.PageEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.PageEventListener;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall.CallType;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.table.TableBuilder;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Lazy
@Service
@GuiProfile
public class PageServiceImpl implements PageService {
private static final LocTextKey CONFIRM_DEACTIVATION_NO_DEP_KEY =
new LocTextKey("sebserver.dialog.confirm.deactivation.noDependencies");
private static final String CONFIRM_DEACTIVATION_KEY = "sebserver.dialog.confirm.deactivation";
private static final Logger log = LoggerFactory.getLogger(PageServiceImpl.class);
private static final LocTextKey MSG_GO_AWAY_FROM_EDIT =
new LocTextKey("sebserver.overall.action.goAwayFromEditPageConfirm");
private static final String ATTR_PAGE_STATE = "PAGE_STATE";
private static final ListenerComparator LIST_COMPARATOR = new ListenerComparator();
private final JSONMapper jsonMapper;
private final WidgetFactory widgetFactory;
private final PolyglotPageService polyglotPageService;
private final ResourceService resourceService;
private final CurrentUser currentUser;
public PageServiceImpl(
final JSONMapper jsonMapper,
final WidgetFactory widgetFactory,
final PolyglotPageService polyglotPageService,
final ResourceService resourceService,
final CurrentUser currentUser) {
this.jsonMapper = jsonMapper;
this.widgetFactory = widgetFactory;
this.polyglotPageService = polyglotPageService;
this.resourceService = resourceService;
this.currentUser = currentUser;
}
@Override
public WidgetFactory getWidgetFactory() {
return this.widgetFactory;
}
@Override
public PolyglotPageService getPolyglotPageService() {
return this.polyglotPageService;
}
@Override
public AuthorizationContextHolder getAuthorizationContextHolder() {
return this.currentUser.getAuthorizationContextHolder();
}
@Override
public I18nSupport getI18nSupport() {
return this.widgetFactory.getI18nSupport();
}
@Override
public ResourceService getResourceService() {
return this.resourceService;
}
@Override
public JSONMapper getJSONMapper() {
return this.jsonMapper;
}
@Override
public RestService getRestService() {
if (this.resourceService == null) {
return null;
}
return this.resourceService.getRestService();
}
@Override
public CurrentUser getCurrentUser() {
return this.currentUser;
}
@Override
public PageState getCurrentState() {
try {
final HttpSession httpSession = RWT
.getUISession()
.getHttpSession();
return (PageState) httpSession.getAttribute(ATTR_PAGE_STATE);
} catch (final Exception e) {
log.error("Failed to get current PageState: ", e);
return null;
}
}
@Override
@SuppressWarnings("unchecked")
public <T extends PageEvent> void firePageEvent(final T event, final PageContext pageContext) {
final Class<? extends PageEvent> typeClass = event.getClass();
final List<PageEventListener<T>> listeners = new ArrayList<>();
ComposerService.traversePageTree(
pageContext.getRoot(),
c -> {
final PageEventListener<?> listener =
(PageEventListener<?>) c.getData(PageEventListener.LISTENER_ATTRIBUTE_KEY);
return listener != null && listener.match(typeClass);
},
c -> listeners.add(((PageEventListener<T>) c.getData(PageEventListener.LISTENER_ATTRIBUTE_KEY))));
if (listeners.isEmpty()) {
return;
}
listeners.stream()
.sorted(LIST_COMPARATOR)
.forEach(listener -> {
try {
listener.notify(event);
} catch (final Exception e) {
log.error("Unexpected error while notify PageEventListener: ", e);
}
});
}
@Override
public void executePageAction(final PageAction pageAction, final Consumer<Result<PageAction>> callback) {
final PageState currentState = getCurrentState();
if (!pageAction.ignoreMoveAwayFromEdit && currentState != null && currentState.type() == Type.FORM_EDIT) {
pageAction.pageContext().applyConfirmDialog(
MSG_GO_AWAY_FROM_EDIT,
confirm -> {
if (confirm) {
exec(pageAction, callback);
} else {
callback.accept(Result.ofRuntimeError("Confirm denied"));
}
});
} else {
exec(pageAction, callback);
}
}
@Override
public <T extends Entity & Activatable> Supplier<LocTextKey> confirmDeactivation(final Set<? extends T> entities) {
final RestService restService = this.resourceService.getRestService();
return () -> {
if (entities == null || entities.isEmpty()) {
return null;
}
try {
final int dependencies = (int) entities.stream()
.flatMap(entity -> {
final RestCall<Set<EntityKey>>.RestCallBuilder builder =
restService.<Set<EntityKey>>getBuilder(
entity.entityType(),
CallType.GET_DEPENDENCIES);
return builder
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(entity.getModelId()))
.withQueryParam(API.PARAM_BULK_ACTION_TYPE, BulkActionType.DEACTIVATE.name())
.call()
.getOrThrow().stream();
}).count();
if (dependencies > 0) {
return new LocTextKey(CONFIRM_DEACTIVATION_KEY, String.valueOf(dependencies));
} else {
return CONFIRM_DEACTIVATION_NO_DEP_KEY;
}
} catch (final Exception e) {
log.warn("Failed to get dependencies. Error: {}", e.getMessage());
return new LocTextKey(CONFIRM_DEACTIVATION_KEY, "");
}
};
}
@Override
public <T extends Entity & Activatable> Function<PageAction, PageAction> activationToggleActionFunction(
final EntityTable<T> table,
final LocTextKey noSelectionText,
Function<PageAction, PageAction> testBeforeActivation) {
return action -> {
final Set<T> selectedROWData = table.getSelectedROWData();
if (selectedROWData == null || selectedROWData.isEmpty()) {
throw new PageMessageException(noSelectionText);
}
final RestService restService = this.resourceService.getRestService();
final EntityType entityType = table.getEntityType();
final Collection<Exception> errors = new ArrayList<>();
for (final T entity : selectedROWData) {
if (!entity.isActive()) {
RestCall<T>.RestCallBuilder restCallBuilder = restService.<T>getBuilder(
entityType,
CallType.ACTIVATION_ACTIVATE)
.withURIVariable(API.PARAM_MODEL_ID, entity.getModelId());
if (testBeforeActivation != null) {
try {
action.withEntityKey(entity.getEntityKey());
testBeforeActivation.apply(action);
restCallBuilder
.call()
.onError(errors::add);
} catch (Exception e) {
errors.add(e);
}
} else {
restCallBuilder
.call()
.onError(errors::add);
}
} else {
restService.<T>getBuilder(entityType, CallType.ACTIVATION_DEACTIVATE)
.withURIVariable(API.PARAM_MODEL_ID, entity.getModelId())
.call()
.onError(errors::add);
}
}
if (!errors.isEmpty()) {
final String entityTypeName = this.resourceService.getEntityTypeName(entityType);
throw new MultiPageMessageException(
new LocTextKey(PageContext.GENERIC_ACTIVATE_ERROR_TEXT_KEY, entityTypeName),
errors);
}
return action;
};
}
private void exec(final PageAction pageAction, final Consumer<Result<PageAction>> callback) {
pageAction.applyAction(result -> {
if (!result.hasError()) {
final PageAction action = result.get();
if (pageAction.fireActionEvent) {
firePageEvent(new ActionEvent(action), action.pageContext());
}
try {
final HttpSession httpSession = RWT
.getUISession()
.getHttpSession();
if (action != null &&
action.fireActionEvent &&
action.definition != null &&
action.definition.targetState != null) {
final PageState pageState = new PageState(action.definition.targetState, action);
if (log.isDebugEnabled()) {
log.debug("Set session PageState: {} : {}", pageState, httpSession.getId());
}
httpSession.setAttribute(ATTR_PAGE_STATE, pageState);
}
} catch (final Exception e) {
log.error("Failed to set current PageState: ", e);
}
}
callback.accept(result);
});
}
@Override
public void publishAction(final PageAction pageAction, final boolean active) {
this.firePageEvent(new ActionPublishEvent(pageAction, active), pageAction.pageContext());
}
@Override
public FormBuilder formBuilder(final PageContext pageContext, final int rows) {
return new FormBuilder(this, pageContext, rows);
}
@Override
public <T extends Entity> TableBuilder<T> entityTableBuilder(
final String name,
final RestCall<Page<T>> apiCall) {
return new TableBuilder<>(name, this, apiCall);
}
@Override
public void logout(final PageContext pageContext) {
this.clearState();
try {
final boolean logoutSuccessful = this.currentUser.logout();
if (!logoutSuccessful) {
log.warn("Failed to logout. See logfiles for more information");
}
} catch (final Exception e) {
log.info("Cleanup logout failed: {}", e.getMessage());
} finally {
pageContext.forwardToLoginPage();
}
}
@Override
public void clearState() {
try {
final HttpSession httpSession = RWT
.getUISession()
.getHttpSession();
log.debug("Clear session PageState: {}", httpSession.getId());
httpSession.removeAttribute(ATTR_PAGE_STATE);
} catch (final Exception e) {
log.error("Failed to clear current PageState: ", e);
}
}
private static final class ListenerComparator implements Comparator<PageEventListener<?>>, Serializable {
private static final long serialVersionUID = 2571739214439340404L;
@Override
public int compare(final PageEventListener<?> o1, final PageEventListener<?> o2) {
return Integer.compare(o1.priority(), o2.priority());
}
}
}

View file

@ -1,195 +1,191 @@
/*
* 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.session;
import java.util.Collection;
import java.util.EnumMap;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Display;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
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.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor;
public class ClientConnectionDetails {
private static final Logger log = LoggerFactory.getLogger(ClientConnectionDetails.class);
private final static LocTextKey EXAM_NAME_TEXT_KEY =
new LocTextKey("sebserver.monitoring.connection.list.column.examname");
private final static LocTextKey CONNECTION_ID_TEXT_KEY =
new LocTextKey("sebserver.monitoring.connection.list.column.id");
private final static LocTextKey CONNECTION_ADDRESS_TEXT_KEY =
new LocTextKey("sebserver.monitoring.connection.list.column.address");
private final static LocTextKey CONNECTION_STATUS_TEXT_KEY =
new LocTextKey("sebserver.monitoring.connection.list.column.status");
private static final int NUMBER_OF_NONE_INDICATOR_ROWS = 3;
private final PageService pageService;
private final ResourceService resourceService;
private final Exam exam;
private final EnumMap<IndicatorType, IndicatorData> indicatorMapping;
private final RestCall<ClientConnectionData>.RestCallBuilder restCallBuilder;
private final FormHandle<?> formhandle;
private final ColorData colorData;
private ClientConnectionData connectionData = null;
private boolean statusChanged = true;
public ClientConnectionDetails(
final PageService pageService,
final PageContext pageContext,
final Exam exam,
final RestCall<ClientConnectionData>.RestCallBuilder restCallBuilder,
final Collection<Indicator> indicators) {
final Display display = pageContext.getRoot().getDisplay();
this.pageService = pageService;
this.resourceService = pageService.getResourceService();
this.exam = exam;
this.restCallBuilder = restCallBuilder;
this.colorData = new ColorData(display);
this.indicatorMapping = IndicatorData.createFormIndicators(
indicators,
display,
this.colorData,
NUMBER_OF_NONE_INDICATOR_ROWS);
final FormBuilder formBuilder = this.pageService.formBuilder(pageContext)
.readonly(true)
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
EXAM_NAME_TEXT_KEY,
this.exam.getName()))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
CONNECTION_ID_TEXT_KEY,
Constants.EMPTY_NOTE))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS,
CONNECTION_ADDRESS_TEXT_KEY,
Constants.EMPTY_NOTE))
.withDefaultSpanInput(3)
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_STATUS,
CONNECTION_STATUS_TEXT_KEY,
Constants.EMPTY_NOTE)
.asColorbox())
.addEmptyCell();
this.indicatorMapping
.values()
.stream()
.forEach(indData -> {
formBuilder.addField(FormBuilder.text(
indData.indicator.name,
new LocTextKey(indData.indicator.name),
Constants.EMPTY_NOTE)
.asColorbox()
.withDefaultLabel(indData.indicator.name))
.addEmptyCell();
});
this.formhandle = formBuilder.build();
}
public void updateData(final ServerPushContext context) {
final ClientConnectionData connectionData = this.restCallBuilder
.call()
.get(error -> {
log.error("Unexpected error while trying to get current client connection data: ", error);
return null;
});
if (this.connectionData != null && connectionData != null) {
this.statusChanged =
this.connectionData.clientConnection.status != connectionData.clientConnection.status ||
this.connectionData.missingPing != connectionData.missingPing;
}
this.connectionData = connectionData;
}
public void updateGUI(final ServerPushContext context) {
if (this.connectionData == null) {
return;
}
final Form form = this.formhandle.getForm();
form.setFieldValue(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
this.connectionData.clientConnection.userSessionId);
form.setFieldValue(
Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS,
this.connectionData.clientConnection.clientAddress);
if (this.statusChanged) {
// update status
form.setFieldValue(
Domain.CLIENT_CONNECTION.ATTR_STATUS,
this.resourceService.localizedClientConnectionStatusName(this.connectionData));
final Color statusColor = this.colorData.getStatusColor(this.connectionData);
final Color statusTextColor = this.colorData.getStatusTextColor(statusColor);
form.setFieldColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusColor);
form.setFieldTextColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusTextColor);
}
// update indicators
this.connectionData.getIndicatorValues()
.stream()
.forEach(indValue -> {
final IndicatorData indData = this.indicatorMapping.get(indValue.getType());
final double value = indValue.getValue();
final String displayValue = IndicatorValue.getDisplayValue(indValue);
if (!this.connectionData.clientConnection.status.establishedStatus) {
form.setFieldValue(
indData.indicator.name,
(indData.indicator.type.showOnlyInActiveState)
? Constants.EMPTY_NOTE
: displayValue);
form.setFieldColor(indData.indicator.name, indData.defaultColor);
form.setFieldTextColor(indData.indicator.name, indData.defaultTextColor);
} else {
form.setFieldValue(indData.indicator.name, displayValue);
final int weight = IndicatorData.getWeight(indData, value);
if (weight >= 0 && weight < indData.thresholdColor.length) {
final ThresholdColor thresholdColor = indData.thresholdColor[weight];
form.setFieldColor(indData.indicator.name, thresholdColor.color);
form.setFieldTextColor(indData.indicator.name, thresholdColor.textColor);
} else {
form.setFieldColor(indData.indicator.name, indData.defaultColor);
form.setFieldTextColor(indData.indicator.name, indData.defaultTextColor);
}
}
});
}
}
/*
* 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.session;
import java.util.Collection;
import java.util.EnumMap;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Display;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
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.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor;
public class ClientConnectionDetails {
private static final Logger log = LoggerFactory.getLogger(ClientConnectionDetails.class);
private final static LocTextKey EXAM_NAME_TEXT_KEY =
new LocTextKey("sebserver.monitoring.connection.form.exam");
private final static LocTextKey CONNECTION_ID_TEXT_KEY =
new LocTextKey("sebserver.monitoring.connection.form.id");
private final static LocTextKey CONNECTION_ADDRESS_TEXT_KEY =
new LocTextKey("sebserver.monitoring.connection.form.address");
private final static LocTextKey CONNECTION_STATUS_TEXT_KEY =
new LocTextKey("sebserver.monitoring.connection.form.status");
private static final int NUMBER_OF_NONE_INDICATOR_ROWS = 3;
private final PageService pageService;
private final ResourceService resourceService;
private final Exam exam;
private final EnumMap<IndicatorType, IndicatorData> indicatorMapping;
private final RestCall<ClientConnectionData>.RestCallBuilder restCallBuilder;
private final FormHandle<?> formhandle;
private final ColorData colorData;
private ClientConnectionData connectionData = null;
private boolean statusChanged = true;
public ClientConnectionDetails(
final PageService pageService,
final PageContext pageContext,
final Exam exam,
final RestCall<ClientConnectionData>.RestCallBuilder restCallBuilder,
final Collection<Indicator> indicators) {
final Display display = pageContext.getRoot().getDisplay();
this.pageService = pageService;
this.resourceService = pageService.getResourceService();
this.exam = exam;
this.restCallBuilder = restCallBuilder;
this.colorData = new ColorData(display);
this.indicatorMapping = IndicatorData.createFormIndicators(
indicators,
display,
this.colorData,
NUMBER_OF_NONE_INDICATOR_ROWS);
final FormBuilder formBuilder = this.pageService.formBuilder(pageContext)
.readonly(true)
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
EXAM_NAME_TEXT_KEY,
this.exam.getName()))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
CONNECTION_ID_TEXT_KEY,
Constants.EMPTY_NOTE))
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS,
CONNECTION_ADDRESS_TEXT_KEY,
Constants.EMPTY_NOTE))
.withDefaultSpanInput(3)
.addField(FormBuilder.text(
Domain.CLIENT_CONNECTION.ATTR_STATUS,
CONNECTION_STATUS_TEXT_KEY,
Constants.EMPTY_NOTE)
.asColorBox())
.addEmptyCell();
this.indicatorMapping
.values()
.forEach(indData -> formBuilder.addField(FormBuilder.text(
indData.indicator.name,
new LocTextKey(indData.indicator.name),
Constants.EMPTY_NOTE)
.asColorBox()
.withDefaultLabel(indData.indicator.name))
.addEmptyCell());
this.formhandle = formBuilder.build();
}
public void updateData() {
final ClientConnectionData connectionData = this.restCallBuilder
.call()
.get(error -> {
log.error("Unexpected error while trying to get current client connection data: ", error);
return null;
});
if (this.connectionData != null && connectionData != null) {
this.statusChanged =
this.connectionData.clientConnection.status != connectionData.clientConnection.status ||
this.connectionData.missingPing != connectionData.missingPing;
}
this.connectionData = connectionData;
}
public void updateGUI() {
if (this.connectionData == null) {
return;
}
final Form form = this.formhandle.getForm();
form.setFieldValue(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
this.connectionData.clientConnection.userSessionId);
form.setFieldValue(
Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS,
this.connectionData.clientConnection.clientAddress);
if (this.statusChanged) {
// update status
form.setFieldValue(
Domain.CLIENT_CONNECTION.ATTR_STATUS,
this.resourceService.localizedClientConnectionStatusName(this.connectionData));
final Color statusColor = this.colorData.getStatusColor(this.connectionData);
final Color statusTextColor = this.colorData.getStatusTextColor(statusColor);
form.setFieldColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusColor);
form.setFieldTextColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusTextColor);
}
// update indicators
this.connectionData.getIndicatorValues()
.forEach(indValue -> {
final IndicatorData indData = this.indicatorMapping.get(indValue.getType());
final double value = indValue.getValue();
final String displayValue = IndicatorValue.getDisplayValue(indValue);
if (!this.connectionData.clientConnection.status.establishedStatus) {
form.setFieldValue(
indData.indicator.name,
(indData.indicator.type.showOnlyInActiveState)
? Constants.EMPTY_NOTE
: displayValue);
form.setFieldColor(indData.indicator.name, indData.defaultColor);
form.setFieldTextColor(indData.indicator.name, indData.defaultTextColor);
} else {
form.setFieldValue(indData.indicator.name, displayValue);
final int weight = IndicatorData.getWeight(indData, value);
if (weight >= 0 && weight < indData.thresholdColor.length) {
final ThresholdColor thresholdColor = indData.thresholdColor[weight];
form.setFieldColor(indData.indicator.name, thresholdColor.color);
form.setFieldTextColor(indData.indicator.name, thresholdColor.textColor);
} else {
form.setFieldColor(indData.indicator.name, indData.defaultColor);
form.setFieldTextColor(indData.indicator.name, indData.defaultTextColor);
}
}
});
}
}

View file

@ -104,7 +104,6 @@ public final class MultiSelectionCheckbox extends Composite implements Selection
}
Arrays.asList(StringUtils.split(keys, Constants.LIST_SEPARATOR))
.stream()
.forEach(key -> {
final Button button = this.checkboxes.get(key);
if (button != null) {
@ -120,7 +119,7 @@ public final class MultiSelectionCheckbox extends Composite implements Selection
this.checkboxes
.values()
.stream()
.filter(button -> button.getSelection())
.filter(Button::getSelection)
.map(button -> (String) button.getData(OPTION_VALUE))
.collect(Collectors.toList()).toArray());
}
@ -129,7 +128,6 @@ public final class MultiSelectionCheckbox extends Composite implements Selection
public void clear() {
this.checkboxes
.values()
.stream()
.forEach(button -> button.setSelection(false));
}

View file

@ -1,242 +1,249 @@
/*
* 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.widget;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
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;
public final class ThresholdList extends Composite {
private static final Logger log = LoggerFactory.getLogger(ThresholdList.class);
private static final long serialVersionUID = -2305091471607040280L;
private static final int ACTION_COLUMN_WIDTH = 20;
private static final String COLOR_SELECTION_TEXT_KEY = "sebserver.exam.indicator.thresholds.select.color";
private static final LocTextKey VALUE_TEXT_KEY = new LocTextKey("sebserver.exam.indicator.thresholds.list.value");
private static final LocTextKey COLOR_TEXT_KEY = new LocTextKey("sebserver.exam.indicator.thresholds.list.color");
private static final LocTextKey ADD_TEXT_KEY = new LocTextKey("sebserver.exam.indicator.thresholds.list.add");
private static final LocTextKey REMOVE_TEXT_KEY = new LocTextKey("sebserver.exam.indicator.thresholds.list.remove");
private final WidgetFactory widgetFactory;
private final Supplier<IndicatorType> indicatorTypeSupplier;
private final List<Entry> thresholds = new ArrayList<>();
private final GridData valueCell;
private final GridData colorCell;
private final GridData actionCell;
private final Composite updateAnchor;
ThresholdList(
final Composite parent,
final Composite updateAnchor,
final WidgetFactory widgetFactory,
final Supplier<IndicatorType> indicatorTypeSupplier) {
super(parent, SWT.NONE);
this.indicatorTypeSupplier = indicatorTypeSupplier;
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;
gridLayout.marginLeft = 0;
gridLayout.marginHeight = 0;
gridLayout.marginWidth = 0;
gridLayout.horizontalSpacing = 0;
setLayout(gridLayout);
this.addListener(SWT.Resize, this::adaptColumnWidth);
final Label valueTitle = widgetFactory.labelLocalized(
this,
CustomVariant.TITLE_LABEL,
VALUE_TEXT_KEY);
this.valueCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
valueTitle.setLayoutData(this.valueCell);
final Label colorTitle = widgetFactory.labelLocalized(
this,
CustomVariant.TITLE_LABEL,
COLOR_TEXT_KEY);
this.colorCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
colorTitle.setLayoutData(this.colorCell);
final Label imageButton = widgetFactory.imageButton(
ImageIcon.ADD_BOX,
this,
ADD_TEXT_KEY,
this::addThreshold);
this.actionCell = new GridData(SWT.LEFT, SWT.CENTER, true, false);
imageButton.setLayoutData(this.actionCell);
}
public void setThresholds(final Collection<Threshold> thresholds) {
clearList();
if (thresholds != null) {
thresholds
.stream()
.forEach(this::addThreshold);
}
}
public Collection<Threshold> getThresholds() {
removeInvalidListEntries();
return this.thresholds
.stream()
.map(entry -> new Threshold(entry.getValue(), entry.getColor()))
.collect(Collectors.toList());
}
private void removeInvalidListEntries() {
this.thresholds
.stream()
.filter(entry -> entry.getValue() == null || StringUtils.isBlank(entry.getColor()))
.collect(Collectors.toList())
.stream()
.forEach(entry -> removeThreshold(entry));
}
private void clearList() {
this.thresholds.stream()
.forEach(e -> e.dispose());
this.thresholds.clear();
}
private void addThreshold(final Event event) {
addThreshold((Threshold) null);
}
private void addThreshold(final Threshold threshold) {
final Text valueInput = this.widgetFactory.numberInput(
this, s -> {
if (this.indicatorTypeSupplier.get().integerValue) {
Integer.parseInt(s);
} else {
Double.parseDouble(s);
}
});
final GridData valueCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
valueInput.setLayoutData(valueCell);
final Selection selector = this.widgetFactory.selectionLocalized(
Type.COLOR, this, null, null, null,
COLOR_SELECTION_TEXT_KEY);
final GridData selectorCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
selectorCell.horizontalIndent = 2;
selector.adaptToControl().setLayoutData(selectorCell);
final Label imageButton = this.widgetFactory.imageButton(
ImageIcon.REMOVE_BOX,
this,
REMOVE_TEXT_KEY,
null);
final GridData actionCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
imageButton.setLayoutData(actionCell);
if (threshold != null) {
if (threshold.value != null) {
valueInput.setText(Indicator.getDisplayValue(
this.indicatorTypeSupplier.get(),
threshold.value));
}
if (threshold.color != null) {
selector.select(threshold.color);
}
}
final Entry entry = new Entry(valueInput, selector, imageButton);
this.thresholds.add(entry);
this.updateAnchor.layout();
PageService.updateScrolledComposite(this);
}
private void removeThreshold(final Entry entry) {
if (this.thresholds.remove(entry)) {
entry.dispose();
}
this.updateAnchor.layout();
PageService.updateScrolledComposite(this);
}
private void adaptColumnWidth(final Event event) {
try {
final int currentTableWidth = this.getClientArea().width;
final int dynWidth = currentTableWidth - ACTION_COLUMN_WIDTH;
final int colWidth = dynWidth / 2;
this.valueCell.widthHint = colWidth;
this.colorCell.widthHint = colWidth;
this.layout();
} catch (final Exception e) {
log.warn("Failed to adaptColumnWidth: ", e);
}
}
private final class Entry {
final Text valueInput;
final Selection colorSelector;
final Label removeButton;
Entry(final Text valueInput, final Selection colorSelector, final Label removeButton) {
super();
this.valueInput = valueInput;
this.colorSelector = colorSelector;
this.removeButton = removeButton;
removeButton.addListener(SWT.MouseDown, event -> removeThreshold(this));
}
void dispose() {
this.valueInput.dispose();
this.colorSelector.adaptToControl().dispose();
this.removeButton.dispose();
}
Double getValue() {
if (this.valueInput == null || StringUtils.isBlank(this.valueInput.getText())) {
return null;
}
return Double.parseDouble(this.valueInput.getText());
}
String getColor() {
if (this.colorSelector == null) {
return null;
}
return this.colorSelector.getSelectionValue();
}
}
}
/*
* 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.widget;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.gbl.Constants;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
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;
public final class ThresholdList extends Composite {
private static final Logger log = LoggerFactory.getLogger(ThresholdList.class);
private static final long serialVersionUID = -2305091471607040280L;
private static final int ACTION_COLUMN_WIDTH = 20;
private static final String COLOR_SELECTION_TEXT_KEY = "sebserver.exam.indicator.thresholds.select.color";
private static final LocTextKey VALUE_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.thresholds.list.value");
private static final LocTextKey VALUE_TOOLTIP_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.thresholds.list.value" + Constants.TOOLTIP_TEXT_KEY_SUFFIX);
private static final LocTextKey COLOR_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.thresholds.list.color");
private static final LocTextKey COLOR_TOOLTIP_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.thresholds.list.color" + Constants.TOOLTIP_TEXT_KEY_SUFFIX);
private static final LocTextKey ADD_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.thresholds.list.add");
private static final LocTextKey REMOVE_TEXT_KEY =
new LocTextKey("sebserver.exam.indicator.thresholds.list.remove");
private final WidgetFactory widgetFactory;
private final Supplier<IndicatorType> indicatorTypeSupplier;
private final List<Entry> thresholds = new ArrayList<>();
private final GridData valueCell;
private final GridData colorCell;
private final GridData actionCell;
private final Composite updateAnchor;
ThresholdList(
final Composite parent,
final Composite updateAnchor,
final WidgetFactory widgetFactory,
final Supplier<IndicatorType> indicatorTypeSupplier) {
super(parent, SWT.NONE);
this.indicatorTypeSupplier = indicatorTypeSupplier;
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;
gridLayout.marginLeft = 0;
gridLayout.marginHeight = 0;
gridLayout.marginWidth = 0;
gridLayout.horizontalSpacing = 0;
setLayout(gridLayout);
this.addListener(SWT.Resize, this::adaptColumnWidth);
final Label valueTitle = widgetFactory.labelLocalized(
this,
CustomVariant.TITLE_LABEL,
VALUE_TEXT_KEY,
VALUE_TOOLTIP_TEXT_KEY);
this.valueCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
valueTitle.setLayoutData(this.valueCell);
final Label colorTitle = widgetFactory.labelLocalized(
this,
CustomVariant.TITLE_LABEL,
COLOR_TEXT_KEY,
COLOR_TOOLTIP_TEXT_KEY);
this.colorCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
colorTitle.setLayoutData(this.colorCell);
final Label imageButton = widgetFactory.imageButton(
ImageIcon.ADD_BOX,
this,
ADD_TEXT_KEY,
this::addThreshold);
this.actionCell = new GridData(SWT.LEFT, SWT.CENTER, true, false);
imageButton.setLayoutData(this.actionCell);
}
public void setThresholds(final Collection<Threshold> thresholds) {
clearList();
if (thresholds != null) {
thresholds.forEach(this::addThreshold);
}
}
public Collection<Threshold> getThresholds() {
removeInvalidListEntries();
return this.thresholds
.stream()
.map(entry -> new Threshold(entry.getValue(), entry.getColor()))
.collect(Collectors.toList());
}
private void removeInvalidListEntries() {
this.thresholds
.stream()
.filter(entry -> entry.getValue() == null || StringUtils.isBlank(entry.getColor()))
.collect(Collectors.toList())
.forEach(this::removeThreshold);
}
private void clearList() {
this.thresholds.forEach(Entry::dispose);
this.thresholds.clear();
}
private void addThreshold(final Event event) {
addThreshold((Threshold) null);
}
private void addThreshold(final Threshold threshold) {
final Text valueInput = this.widgetFactory.numberInput(
this, s -> {
if (this.indicatorTypeSupplier.get().integerValue) {
Integer.parseInt(s);
} else {
Double.parseDouble(s);
}
});
final GridData valueCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
valueInput.setLayoutData(valueCell);
final Selection selector = this.widgetFactory.selectionLocalized(
Type.COLOR, this, null, null, null,
COLOR_SELECTION_TEXT_KEY);
final GridData selectorCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
selectorCell.horizontalIndent = 2;
selector.adaptToControl().setLayoutData(selectorCell);
final Label imageButton = this.widgetFactory.imageButton(
ImageIcon.REMOVE_BOX,
this,
REMOVE_TEXT_KEY,
null);
final GridData actionCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
imageButton.setLayoutData(actionCell);
if (threshold != null) {
if (threshold.value != null) {
valueInput.setText(Indicator.getDisplayValue(
this.indicatorTypeSupplier.get(),
threshold.value));
}
if (threshold.color != null) {
selector.select(threshold.color);
}
}
final Entry entry = new Entry(valueInput, selector, imageButton);
this.thresholds.add(entry);
this.updateAnchor.layout();
PageService.updateScrolledComposite(this);
}
private void removeThreshold(final Entry entry) {
if (this.thresholds.remove(entry)) {
entry.dispose();
}
this.updateAnchor.layout();
PageService.updateScrolledComposite(this);
}
private void adaptColumnWidth(final Event event) {
try {
final int currentTableWidth = this.getClientArea().width;
final int dynWidth = currentTableWidth - ACTION_COLUMN_WIDTH;
final int colWidth = dynWidth / 2;
this.valueCell.widthHint = colWidth;
this.colorCell.widthHint = colWidth;
this.layout();
} catch (final Exception e) {
log.warn("Failed to adaptColumnWidth: ", e);
}
}
private final class Entry {
final Text valueInput;
final Selection colorSelector;
final Label removeButton;
Entry(final Text valueInput, final Selection colorSelector, final Label removeButton) {
super();
this.valueInput = valueInput;
this.colorSelector = colorSelector;
this.removeButton = removeButton;
removeButton.addListener(SWT.MouseDown, event -> removeThreshold(this));
}
void dispose() {
this.valueInput.dispose();
this.colorSelector.adaptToControl().dispose();
this.removeButton.dispose();
}
Double getValue() {
if (this.valueInput == null || StringUtils.isBlank(this.valueInput.getText())) {
return null;
}
return Double.parseDouble(this.valueInput.getText());
}
String getColor() {
if (this.colorSelector == null) {
return null;
}
return this.colorSelector.getSelectionValue();
}
}
}

View file

@ -1,293 +1,290 @@
/*
* 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.webservice.servicelayer.lms.impl.moodle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
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.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
/** Implements the LmsAPITemplate for Open edX LMS Course API access.
*
* See also: https://docs.moodle.org/dev/Web_service_API_functions */
public class MoodleCourseAccess extends CourseAccess {
private static final Logger log = LoggerFactory.getLogger(MoodleCourseAccess.class);
private static final String MOOLDE_QUIZ_START_URL_PATH = "/mod/quiz/view.php?id=";
private static final String MOODLE_COURSE_API_FUNCTION_NAME = "core_course_get_courses";
private static final String MOODLE_QUIZ_API_FUNCTION_NAME = "mod_quiz_get_quizzes_by_courses";
private final JSONMapper jsonMapper;
private final LmsSetup lmsSetup;
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private MoodleAPIRestTemplate restTemplate;
protected MoodleCourseAccess(
final JSONMapper jsonMapper,
final LmsSetup lmsSetup,
final MoodleRestTemplateFactory moodleRestTemplateFactory,
final AsyncService asyncService) {
super(asyncService);
this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup;
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
}
LmsSetupTestResult initAPIAccess() {
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
if (!attributesCheck.isOk()) {
return attributesCheck;
}
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
if (restTemplateRequest.hasError()) {
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
this.moodleRestTemplateFactory.knownTokenAccessPaths;
log.error(message, restTemplateRequest.getError().getMessage());
return LmsSetupTestResult.ofTokenRequestError(message);
}
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
try {
restTemplate.testAPIConnection(
MOODLE_COURSE_API_FUNCTION_NAME,
MOODLE_QUIZ_API_FUNCTION_NAME);
} catch (final RuntimeException e) {
log.error("Failed to access Open edX course API: ", e);
return LmsSetupTestResult.ofQuizAccessAPIError(e.getMessage());
}
return LmsSetupTestResult.ofOkay();
}
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier() {
return () -> {
return getRestTemplate()
.map(this::collectAllQuizzes)
.getOrThrow();
};
}
private ArrayList<QuizData> collectAllQuizzes(final MoodleAPIRestTemplate restTemplate) {
final String urlPrefix = this.lmsSetup.lmsApiUrl + MOOLDE_QUIZ_START_URL_PATH;
return collectAllCourses(
restTemplate)
.stream()
.reduce(
new ArrayList<QuizData>(),
(list, courseData) -> {
list.addAll(quizDataOf(
this.lmsSetup,
courseData,
urlPrefix));
return list;
},
(list1, list2) -> {
list1.addAll(list2);
return list1;
});
}
private List<CourseData> collectAllCourses(final MoodleAPIRestTemplate restTemplate) {
try {
// first get courses form Moodle...
final String coursesJSON = restTemplate.callMoodleAPIFunction(MOODLE_COURSE_API_FUNCTION_NAME);
final Map<String, CourseData> courseData = this.jsonMapper.<Collection<CourseData>> readValue(
coursesJSON,
new TypeReference<Collection<CourseData>>() {
})
.stream()
.collect(Collectors.toMap(d -> d.id, Function.identity()));
// then get all quizzes of courses and filter
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
attributes.put("courseids", new ArrayList<>(courseData.keySet()));
final String quizzesJSON = restTemplate.callMoodleAPIFunction(
MOODLE_QUIZ_API_FUNCTION_NAME,
attributes);
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
quizzesJSON,
CourseQuizData.class);
courseQuizData.quizzes
.stream()
.forEach(quiz -> {
final CourseData course = courseData.get(quiz.course);
if (course != null) {
course.quizzes.add(quiz);
}
});
return courseData.values()
.stream()
.filter(c -> !c.quizzes.isEmpty())
.collect(Collectors.toList());
} catch (final Exception e) {
throw new RuntimeException("Unexpected exception while trying to get course data: ", e);
}
}
private static List<QuizData> quizDataOf(
final LmsSetup lmsSetup,
final CourseData courseData,
final String uriPrefix) {
final Map<String, String> additionalAttrs = new HashMap<>();
additionalAttrs.clear();
additionalAttrs.put("timecreated", String.valueOf(courseData.timecreated));
additionalAttrs.put("course_shortname", courseData.shortname);
additionalAttrs.put("course_fullname", courseData.fullname);
additionalAttrs.put("course_displayname", courseData.displayname);
additionalAttrs.put("course_summary", courseData.summary);
return courseData.quizzes
.stream()
.map(courseQuizData -> {
final String startURI = uriPrefix + courseData.id;
additionalAttrs.put("timelimit", String.valueOf(courseQuizData.timelimit));
return new QuizData(
courseQuizData.id,
lmsSetup.getInstitutionId(),
lmsSetup.id,
lmsSetup.getLmsType(),
courseQuizData.name,
courseQuizData.intro,
Utils.toDateTimeUTCUnix(courseData.startdate),
Utils.toDateTimeUTCUnix(courseData.enddate),
startURI,
additionalAttrs);
})
.collect(Collectors.toList());
}
private Result<MoodleAPIRestTemplate> getRestTemplate() {
if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
.createRestTemplate();
if (templateRequest.hasError()) {
return templateRequest;
} else {
this.restTemplate = templateRequest.get();
}
}
return Result.of(this.restTemplate);
}
/** Maps the Moodle course API course data */
@JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseData {
final String id;
final String shortname;
final String fullname;
final String displayname;
final String summary;
final Long startdate; // unixtime milliseconds UTC
final Long enddate; // unixtime milliseconds UTC
final Long timecreated; // unixtime milliseconds UTC
final Collection<CourseQuiz> quizzes = new ArrayList<>();
@JsonCreator
protected CourseData(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "shortname") final String shortname,
@JsonProperty(value = "fullname") final String fullname,
@JsonProperty(value = "displayname") final String displayname,
@JsonProperty(value = "summary") final String summary,
@JsonProperty(value = "startdate") final Long startdate,
@JsonProperty(value = "enddate") final Long enddate,
@JsonProperty(value = "timecreated") final Long timecreated) {
this.id = id;
this.shortname = shortname;
this.fullname = fullname;
this.displayname = displayname;
this.summary = summary;
this.startdate = startdate;
this.enddate = enddate;
this.timecreated = timecreated;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseQuizData {
final Collection<CourseQuiz> quizzes;
@JsonCreator
protected CourseQuizData(
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes) {
this.quizzes = quizzes;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseQuiz {
final String id;
final String course;
final String coursemodule;
final String name;
final String intro; // HTML
final Long timelimit; // unixtime milliseconds UTC
@JsonCreator
protected CourseQuiz(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "course") final String course,
@JsonProperty(value = "coursemodule") final String coursemodule,
@JsonProperty(value = "name") final String name,
@JsonProperty(value = "intro") final String intro,
@JsonProperty(value = "timelimit") final Long timelimit) {
this.id = id;
this.course = course;
this.coursemodule = coursemodule;
this.name = name;
this.intro = intro;
this.timelimit = timelimit;
}
}
}
/*
* 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.webservice.servicelayer.lms.impl.moodle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
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.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
/** Implements the LmsAPITemplate for Open edX LMS Course API access.
*
* See also: https://docs.moodle.org/dev/Web_service_API_functions */
public class MoodleCourseAccess extends CourseAccess {
private static final Logger log = LoggerFactory.getLogger(MoodleCourseAccess.class);
private static final String MOODLE_QUIZ_START_URL_PATH = "/mod/quiz/view.php?id=";
private static final String MOODLE_COURSE_API_FUNCTION_NAME = "core_course_get_courses";
private static final String MOODLE_QUIZ_API_FUNCTION_NAME = "mod_quiz_get_quizzes_by_courses";
private static final String MOODLE_COURSE_API_COURSE_IDS = "courseids";
private final JSONMapper jsonMapper;
private final LmsSetup lmsSetup;
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private MoodleAPIRestTemplate restTemplate;
protected MoodleCourseAccess(
final JSONMapper jsonMapper,
final LmsSetup lmsSetup,
final MoodleRestTemplateFactory moodleRestTemplateFactory,
final AsyncService asyncService) {
super(asyncService);
this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup;
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
}
LmsSetupTestResult initAPIAccess() {
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
if (!attributesCheck.isOk()) {
return attributesCheck;
}
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
if (restTemplateRequest.hasError()) {
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
this.moodleRestTemplateFactory.knownTokenAccessPaths;
log.error(message, restTemplateRequest.getError().getMessage());
return LmsSetupTestResult.ofTokenRequestError(message);
}
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
try {
restTemplate.testAPIConnection(
MOODLE_COURSE_API_FUNCTION_NAME,
MOODLE_QUIZ_API_FUNCTION_NAME);
} catch (final RuntimeException e) {
log.error("Failed to access Open edX course API: ", e);
return LmsSetupTestResult.ofQuizAccessAPIError(e.getMessage());
}
return LmsSetupTestResult.ofOkay();
}
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier() {
return () -> getRestTemplate()
.map(this::collectAllQuizzes)
.getOrThrow();
}
private ArrayList<QuizData> collectAllQuizzes(final MoodleAPIRestTemplate restTemplate) {
final String urlPrefix = this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH;
return collectAllCourses(
restTemplate)
.stream()
.reduce(
new ArrayList<>(),
(list, courseData) -> {
list.addAll(quizDataOf(
this.lmsSetup,
courseData,
urlPrefix));
return list;
},
(list1, list2) -> {
list1.addAll(list2);
return list1;
});
}
private List<CourseData> collectAllCourses(final MoodleAPIRestTemplate restTemplate) {
try {
// first get courses form Moodle...
final String coursesJSON = restTemplate.callMoodleAPIFunction(MOODLE_COURSE_API_FUNCTION_NAME);
final Map<String, CourseData> courseData = this.jsonMapper.<Collection<CourseData>> readValue(
coursesJSON,
new TypeReference<Collection<CourseData>>() {
})
.stream()
.collect(Collectors.toMap(d -> d.id, Function.identity()));
// then get all quizzes of courses and filter
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
attributes.put(MOODLE_COURSE_API_COURSE_IDS, new ArrayList<>(courseData.keySet()));
final String quizzesJSON = restTemplate.callMoodleAPIFunction(
MOODLE_QUIZ_API_FUNCTION_NAME,
attributes);
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
quizzesJSON,
CourseQuizData.class);
courseQuizData.quizzes
.forEach(quiz -> {
final CourseData course = courseData.get(quiz.course);
if (course != null) {
course.quizzes.add(quiz);
}
});
return courseData.values()
.stream()
.filter(c -> !c.quizzes.isEmpty())
.collect(Collectors.toList());
} catch (final Exception e) {
throw new RuntimeException("Unexpected exception while trying to get course data: ", e);
}
}
static Map<String, String> additionalAttrs = new HashMap<>();
private static List<QuizData> quizDataOf(
final LmsSetup lmsSetup,
final CourseData courseData,
final String uriPrefix) {
additionalAttrs.clear();
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_CREATION_TIME, String.valueOf(courseData.time_created));
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SHORT_NAME, courseData.short_name);
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_FULL_NAME, courseData.full_name);
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_DISPLAY_NAME, courseData.display_name);
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SUMMARY, courseData.summary);
return courseData.quizzes
.stream()
.map(courseQuizData -> {
final String startURI = uriPrefix + courseData.id;
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit));
return new QuizData(
courseQuizData.id,
lmsSetup.getInstitutionId(),
lmsSetup.id,
lmsSetup.getLmsType(),
courseQuizData.name,
courseQuizData.intro,
Utils.toDateTimeUTCUnix(courseData.start_date),
Utils.toDateTimeUTCUnix(courseData.end_date),
startURI,
additionalAttrs);
})
.collect(Collectors.toList());
}
private Result<MoodleAPIRestTemplate> getRestTemplate() {
if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
.createRestTemplate();
if (templateRequest.hasError()) {
return templateRequest;
} else {
this.restTemplate = templateRequest.get();
}
}
return Result.of(this.restTemplate);
}
/** Maps the Moodle course API course data */
@JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseData {
final String id;
final String short_name;
final String full_name;
final String display_name;
final String summary;
final Long start_date; // unix-time milliseconds UTC
final Long end_date; // unix-time milliseconds UTC
final Long time_created; // unix-time milliseconds UTC
final Collection<CourseQuiz> quizzes = new ArrayList<>();
@JsonCreator
protected CourseData(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "shortname") final String short_name,
@JsonProperty(value = "fullname") final String full_name,
@JsonProperty(value = "displayname") final String display_name,
@JsonProperty(value = "summary") final String summary,
@JsonProperty(value = "startdate") final Long start_date,
@JsonProperty(value = "enddate") final Long end_date,
@JsonProperty(value = "timecreated") final Long time_created) {
this.id = id;
this.short_name = short_name;
this.full_name = full_name;
this.display_name = display_name;
this.summary = summary;
this.start_date = start_date;
this.end_date = end_date;
this.time_created = time_created;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseQuizData {
final Collection<CourseQuiz> quizzes;
@JsonCreator
protected CourseQuizData(
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes) {
this.quizzes = quizzes;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseQuiz {
final String id;
final String course;
final String course_module;
final String name;
final String intro; // HTML
final Long time_limit; // unix-time milliseconds UTC
@JsonCreator
protected CourseQuiz(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "course") final String course,
@JsonProperty(value = "coursemodule") final String course_module,
@JsonProperty(value = "name") final String name,
@JsonProperty(value = "intro") final String intro,
@JsonProperty(value = "timelimit") final Long time_limit) {
this.id = id;
this.course = course;
this.course_module = course_module;
this.name = name;
this.intro = intro;
this.time_limit = time_limit;
}
}
}

View file

@ -1,320 +1,320 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@Lazy
@Service
@WebServiceProfile
public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
private static final Logger log = LoggerFactory.getLogger(ExamConfigUpdateServiceImpl.class);
private final ExamDAO examDAO;
private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ExamSessionService examSessionService;
private final ExamUpdateHandler examUpdateHandler;
protected ExamConfigUpdateServiceImpl(
final ExamDAO examDAO,
final ConfigurationDAO configurationDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ExamSessionService examSessionService,
final ExamUpdateHandler examUpdateHandler) {
this.examDAO = examDAO;
this.configurationDAO = configurationDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.examSessionService = examSessionService;
this.examUpdateHandler = examUpdateHandler;
}
// processing:
// check running exam integrity (No running exam with active SEB client-connection available)
// if OK, create an update-id and for each exam, create an update-lock on DB (This also prevents new SEB client connection attempts during update)
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
// store the new configuration values (into history) so that they take effect
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
// evict each Exam from cache and release the update-lock on DB
@Override
public Result<Collection<Long>> processExamConfigurationChange(final Long configurationNodeId) {
final String updateId = this.examUpdateHandler.createUpdateId();
if (log.isDebugEnabled()) {
log.debug("Process SEB Exam Configuration update for: {} with update-id {}",
configurationNodeId,
updateId);
}
return Result.tryCatch(() -> {
// check running exam integrity (No running exam with active SEB client-connection available)
final Collection<Long> examIdsFirstCheck = checkRunningExamIntegrity(configurationNodeId)
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("Involved exams on fist integrity check: {}", examIdsFirstCheck);
}
// if OK, create an update-lock on DB (This also prevents new SEB client connection attempts during update)
final Collection<Exam> exams = lockForUpdate(examIdsFirstCheck, updateId)
.stream()
.map(Result::getOrThrow)
.collect(Collectors.toList());
final Collection<Long> examsIds = exams
.stream()
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Update-Lock successfully placed for all involved exams: {}", examsIds);
}
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
final Collection<Long> examIdsSecondCheck = checkRunningExamIntegrity(configurationNodeId)
.getOrThrow();
checkIntegrityDoubleCheck(
examIdsFirstCheck,
examIdsSecondCheck);
if (log.isDebugEnabled()) {
log.debug("Involved exams on second integrity check: {}", examIdsSecondCheck);
}
// store the new configuration values (into history) so that they take effect
final Configuration configuration = this.configurationDAO
.saveToHistory(configurationNodeId)
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("Successfully save SEB Exam Configuration: {}", configuration);
}
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
for (final Exam exam : exams) {
if (exam.getStatus() == ExamStatus.RUNNING && BooleanUtils.isTrue(exam.lmsSebRestriction)) {
this.examUpdateHandler
.getSebRestrictionService()
.applySebClientRestriction(exam)
.onError(t -> log.error("Failed to update SEB Client restriction for Exam: {}", exam, t));
}
}
// evict each Exam from cache and release the update-lock on DB
for (final Exam exam : exams) {
this.examSessionService
.flushCache(exam)
.onError(t -> log.error("Failed to flush Exam from cache: {}", exam, t));
}
// release the update-locks on involved exams
for (final Long examId : examIdsFirstCheck) {
this.examDAO.releaseLock(examId, updateId)
.onError(t -> log.error("Failed to release lock for Exam: {}", examId, t));
}
return examIdsFirstCheck;
})
.onError(t -> this.examDAO.forceUnlockAll(updateId));
}
@Override
public <T> Result<T> processExamConfigurationMappingChange(
final ExamConfigurationMap mapping,
final Function<ExamConfigurationMap, Result<T>> changeAction) {
return this.examDAO.byPK(mapping.examId)
.map(exam -> {
// if the exam is not currently running just apply the action
if (exam.status != ExamStatus.RUNNING) {
return changeAction
.apply(mapping)
.getOrThrow();
}
// if the exam is running...
final String updateId = this.examUpdateHandler.createUpdateId();
if (log.isDebugEnabled()) {
log.debug(
"Process SEB Exam Configuration mapping update for: {} with update-id {}",
mapping,
updateId);
}
// check if there are no active client connections for this exam
checkActiveClientConnections(exam);
// lock the exam
this.examDAO
.placeLock(exam.id, updateId)
.getOrThrow();
// check again if there are no new active client connections in the meantime
checkActiveClientConnections(exam);
// apply the referenced change action. On error the change is rolled back and
// this processing returns immediately with the error
final T result = changeAction
.apply(mapping)
.onError(t -> log.error("Fauled to save exam configuration: {}",
mapping.configurationNodeId))
.getOrThrow();
// update seb client restriction if the feature is activated for the exam
if (exam.lmsSebRestriction) {
this.examUpdateHandler
.getSebRestrictionService()
.applySebClientRestriction(exam)
.onError(t -> log.error(
"Failed to update SEB Client restriction for Exam: {}",
exam,
t));
}
// flush the exam cache. If there was an error during flush, it is logged but this process goes on
// and the saved changes are not rolled back
this.examSessionService
.flushCache(exam)
.onError(t -> log.error("Failed to flush cache for exam: {}", exam));
// release the exam lock
this.examDAO
.releaseLock(exam.id, updateId)
.onError(t -> log.error("Failed to release lock for exam: {}", exam));
return result;
})
.onError(t -> this.examDAO.forceUnlock(mapping.examId));
}
private void checkActiveClientConnections(final Exam exam) {
if (this.examSessionService.hasActiveSebClientConnections(exam.id)) {
throw new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,
"Integrity violation: There are currently active SEB Client connection.");
}
}
@Override
public void forceReleaseUpdateLocks(final Long configurationId) {
log.warn(" **** Force release of update-locks for all exams that are related to configuration: {}",
configurationId);
try {
final Configuration config = this.configurationDAO
.byPK(configurationId)
.getOrThrow();
final Collection<Long> involvedExams = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(config.configurationNodeId)
.getOrThrow();
final Collection<Long> examsIds = forceReleaseUpdateLocks(involvedExams)
.stream()
.map(Result::getOrThrow)
.collect(Collectors.toList());
log.info("Successfully released update-locks for exams: {}", examsIds);
} catch (final Exception e) {
log.error("Failed to release update-locks for exam(s)", e);
}
}
@Override
public Collection<Result<Long>> forceReleaseUpdateLocks(final Collection<Long> examIds) {
return examIds
.stream()
.map(this.examDAO::forceUnlock)
.collect(Collectors.toList());
}
@Override
public Result<Collection<Long>> checkRunningExamIntegrity(final Long configurationNodeId) {
final Collection<Long> involvedExams = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(configurationNodeId)
.getOrThrow();
if (involvedExams == null || involvedExams.isEmpty()) {
return Result.of(Collections.emptyList());
}
// check if the configuration is attached to a running exams with active client connections
final long activeConnections = involvedExams
.stream()
.flatMap(examId -> {
return this.examSessionService.getConnectionData(examId, Objects::nonNull)
.getOrThrow()
.stream();
})
.filter(ExamSessionService::isActiveConnection)
.count();
// if we have active SEB client connection on any running exam that
// is involved within the specified configuration change, the change is denied
if (activeConnections > 0) {
return Result.ofError(new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,
"Integrity violation: There are currently active SEB Client connection."));
} else {
// otherwise we return the involved identifiers exams to further processing
return Result.of(involvedExams);
}
}
private void checkIntegrityDoubleCheck(
final Collection<Long> examIdsFirstCheck,
final Collection<Long> examIdsSecondCheck) {
if (examIdsFirstCheck.size() != examIdsSecondCheck.size()) {
throw new IllegalStateException("Running Exam integrity check missmatch. examIdsFirstCheck: "
+ examIdsFirstCheck + " examIdsSecondCheck: " + examIdsSecondCheck);
}
}
private Collection<Result<Exam>> lockForUpdate(final Collection<Long> examIds, final String update) {
return examIds.stream()
.map(id -> this.examDAO.placeLock(id, update))
.collect(Collectors.toList());
}
}
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@Lazy
@Service
@WebServiceProfile
public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
private static final Logger log = LoggerFactory.getLogger(ExamConfigUpdateServiceImpl.class);
private final ExamDAO examDAO;
private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ExamSessionService examSessionService;
private final ExamUpdateHandler examUpdateHandler;
protected ExamConfigUpdateServiceImpl(
final ExamDAO examDAO,
final ConfigurationDAO configurationDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ExamSessionService examSessionService,
final ExamUpdateHandler examUpdateHandler) {
this.examDAO = examDAO;
this.configurationDAO = configurationDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.examSessionService = examSessionService;
this.examUpdateHandler = examUpdateHandler;
}
// processing:
// check running exam integrity (No running exam with active SEB client-connection available)
// if OK, create an update-id and for each exam, create an update-lock on DB (This also prevents new SEB client connection attempts during update)
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
// store the new configuration values (into history) so that they take effect
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
// evict each Exam from cache and release the update-lock on DB
@Override
public Result<Collection<Long>> processExamConfigurationChange(final Long configurationNodeId) {
final String updateId = this.examUpdateHandler.createUpdateId();
if (log.isDebugEnabled()) {
log.debug("Process SEB Exam Configuration update for: {} with update-id {}",
configurationNodeId,
updateId);
}
return Result.tryCatch(() -> {
// check running exam integrity (No running exam with active SEB client-connection available)
final Collection<Long> examIdsFirstCheck = checkRunningExamIntegrity(configurationNodeId)
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("Involved exams on fist integrity check: {}", examIdsFirstCheck);
}
// if OK, create an update-lock on DB (This also prevents new SEB client connection attempts during update)
final Collection<Exam> exams = lockForUpdate(examIdsFirstCheck, updateId)
.stream()
.map(Result::getOrThrow)
.collect(Collectors.toList());
final Collection<Long> examsIds = exams
.stream()
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Update-Lock successfully placed for all involved exams: {}", examsIds);
}
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
final Collection<Long> examIdsSecondCheck = checkRunningExamIntegrity(configurationNodeId)
.getOrThrow();
checkIntegrityDoubleCheck(
examIdsFirstCheck,
examIdsSecondCheck);
if (log.isDebugEnabled()) {
log.debug("Involved exams on second integrity check: {}", examIdsSecondCheck);
}
// store the new configuration values (into history) so that they take effect
final Configuration configuration = this.configurationDAO
.saveToHistory(configurationNodeId)
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("Successfully save SEB Exam Configuration: {}", configuration);
}
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
for (final Exam exam : exams) {
if (exam.getStatus() == ExamStatus.RUNNING && BooleanUtils.isTrue(exam.lmsSebRestriction)) {
this.examUpdateHandler
.getSebRestrictionService()
.applySebClientRestriction(exam)
.onError(t -> log.error("Failed to update SEB Client restriction for Exam: {}", exam, t));
}
}
// evict each Exam from cache and release the update-lock on DB
for (final Exam exam : exams) {
this.examSessionService
.flushCache(exam)
.onError(t -> log.error("Failed to flush Exam from cache: {}", exam, t));
}
// release the update-locks on involved exams
for (final Long examId : examIdsFirstCheck) {
this.examDAO.releaseLock(examId, updateId)
.onError(t -> log.error("Failed to release lock for Exam: {}", examId, t));
}
return examIdsFirstCheck;
})
.onError(t -> this.examDAO.forceUnlockAll(updateId));
}
@Override
public <T> Result<T> processExamConfigurationMappingChange(
final ExamConfigurationMap mapping,
final Function<ExamConfigurationMap, Result<T>> changeAction) {
return this.examDAO.byPK(mapping.examId)
.map(exam -> {
// if the exam is not currently running just apply the action
if (exam.status != ExamStatus.RUNNING) {
return changeAction
.apply(mapping)
.getOrThrow();
}
// if the exam is running...
final String updateId = this.examUpdateHandler.createUpdateId();
if (log.isDebugEnabled()) {
log.debug(
"Process SEB Exam Configuration mapping update for: {} with update-id {}",
mapping,
updateId);
}
// check if there are no active client connections for this exam
checkActiveClientConnections(exam);
// lock the exam
this.examDAO
.placeLock(exam.id, updateId)
.getOrThrow();
// check again if there are no new active client connections in the meantime
checkActiveClientConnections(exam);
// apply the referenced change action. On error the change is rolled back and
// this processing returns immediately with the error
final T result = changeAction
.apply(mapping)
.onError(t -> log.error("Failed to save exam configuration: {}",
mapping.configurationNodeId))
.getOrThrow();
// update seb client restriction if the feature is activated for the exam
if (exam.lmsSebRestriction) {
this.examUpdateHandler
.getSebRestrictionService()
.applySebClientRestriction(exam)
.onError(t -> log.error(
"Failed to update SEB Client restriction for Exam: {}",
exam,
t));
}
// flush the exam cache. If there was an error during flush, it is logged but this process goes on
// and the saved changes are not rolled back
this.examSessionService
.flushCache(exam)
.onError(t -> log.error("Failed to flush cache for exam: {}", exam));
// release the exam lock
this.examDAO
.releaseLock(exam.id, updateId)
.onError(t -> log.error("Failed to release lock for exam: {}", exam));
return result;
})
.onError(t -> this.examDAO.forceUnlock(mapping.examId));
}
private void checkActiveClientConnections(final Exam exam) {
if (this.examSessionService.hasActiveSebClientConnections(exam.id)) {
throw new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,
"Integrity violation: There are currently active SEB Client connection.");
}
}
@Override
public void forceReleaseUpdateLocks(final Long configurationId) {
log.warn(" **** Force release of update-locks for all exams that are related to configuration: {}",
configurationId);
try {
final Configuration config = this.configurationDAO
.byPK(configurationId)
.getOrThrow();
final Collection<Long> involvedExams = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(config.configurationNodeId)
.getOrThrow();
final Collection<Long> examsIds = forceReleaseUpdateLocks(involvedExams)
.stream()
.map(Result::getOrThrow)
.collect(Collectors.toList());
log.info("Successfully released update-locks for exams: {}", examsIds);
} catch (final Exception e) {
log.error("Failed to release update-locks for exam(s)", e);
}
}
@Override
public Collection<Result<Long>> forceReleaseUpdateLocks(final Collection<Long> examIds) {
return examIds
.stream()
.map(this.examDAO::forceUnlock)
.collect(Collectors.toList());
}
@Override
public Result<Collection<Long>> checkRunningExamIntegrity(final Long configurationNodeId) {
final Collection<Long> involvedExams = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(configurationNodeId)
.getOrThrow();
if (involvedExams == null || involvedExams.isEmpty()) {
return Result.of(Collections.emptyList());
}
// check if the configuration is attached to a running exams with active client connections
final long activeConnections = involvedExams
.stream()
.flatMap(examId -> {
return this.examSessionService.getConnectionData(examId, Objects::nonNull)
.getOrThrow()
.stream();
})
.filter(ExamSessionService::isActiveConnection)
.count();
// if we have active SEB client connection on any running exam that
// is involved within the specified configuration change, the change is denied
if (activeConnections > 0) {
return Result.ofError(new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,
"Integrity violation: There are currently active SEB Client connection."));
} else {
// otherwise we return the involved identifiers exams to further processing
return Result.of(involvedExams);
}
}
private void checkIntegrityDoubleCheck(
final Collection<Long> examIdsFirstCheck,
final Collection<Long> examIdsSecondCheck) {
if (examIdsFirstCheck.size() != examIdsSecondCheck.size()) {
throw new IllegalStateException("Running Exam integrity check missmatch. examIdsFirstCheck: "
+ examIdsFirstCheck + " examIdsSecondCheck: " + examIdsSecondCheck);
}
}
private Collection<Result<Exam>> lockForUpdate(final Collection<Long> examIds, final String update) {
return examIds.stream()
.map(id -> this.examDAO.placeLock(id, update))
.collect(Collectors.toList());
}
}

View file

@ -1,244 +1,245 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
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.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
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.user.PasswordChange;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Pair;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfigurationMapRecordDynamicSqlSupport;
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.bulkaction.BulkActionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile
@RestController
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_CONFIGURATION_MAP_ENDPOINT)
public class ExamConfigurationMappingController extends EntityController<ExamConfigurationMap, ExamConfigurationMap> {
private final ExamDAO examDao;
private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigUpdateService examConfigUpdateService;
private final ExamSessionService examSessionService;
protected ExamConfigurationMappingController(
final AuthorizationService authorization,
final BulkActionService bulkActionService,
final EntityDAO<ExamConfigurationMap, ExamConfigurationMap> entityDAO,
final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ExamDAO examDao,
final ConfigurationNodeDAO configurationNodeDAO,
final ExamConfigUpdateService examConfigUpdateService,
final ExamSessionService examSessionService) {
super(
authorization,
bulkActionService,
entityDAO,
userActivityLogDAO,
paginationService,
beanValidationService);
this.examDao = examDao;
this.configurationNodeDAO = configurationNodeDAO;
this.examConfigUpdateService = examConfigUpdateService;
this.examSessionService = examSessionService;
}
@Override
protected ExamConfigurationMap createNew(final POSTMapper postParams) {
final Long institutionId = postParams.getLong(API.PARAM_INSTITUTION_ID);
return new ExamConfigurationMap(institutionId, postParams);
}
@Override
protected SqlTable getSQLTableOfEntity() {
return ExamConfigurationMapRecordDynamicSqlSupport.examConfigurationMapRecord;
}
@Override
protected EntityType getGrantEntityType() {
return EntityType.EXAM;
}
@Override
protected GrantEntity toGrantEntity(final ExamConfigurationMap entity) {
if (entity == null) {
return null;
}
return this.examDao
.byPK(entity.examId)
.getOrThrow();
}
@Override
protected Result<ExamConfigurationMap> checkCreateAccess(final ExamConfigurationMap entity) {
final GrantEntity grantEntity = toGrantEntity(entity);
this.authorization.checkWrite(grantEntity);
return Result.of(entity);
}
@Override
protected Result<ExamConfigurationMap> validForCreate(final ExamConfigurationMap entity) {
return super.validForCreate(entity)
.map(this::checkConfigurationState)
.map(this::checkPasswordMatch)
.map(this::checkNoActiveClientConnections);
}
@Override
@RequestMapping(
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ExamConfigurationMap create(
@RequestParam final MultiValueMap<String, String> allRequestParams,
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
// check modify privilege for requested institution and concrete entityType
this.checkModifyPrivilege(institutionId);
final POSTMapper postMap = new POSTMapper(allRequestParams)
.putIfAbsent(API.PARAM_INSTITUTION_ID, String.valueOf(institutionId));
final ExamConfigurationMap requestModel = this.createNew(postMap);
return this.checkCreateAccess(requestModel)
.map(this::checkPasswordMatch)
.flatMap(entity -> this.examConfigUpdateService.processExamConfigurationMappingChange(
entity,
this.entityDAO::createNew))
.flatMap(this::logCreate)
.flatMap(this::notifyCreated)
.getOrThrow();
}
@Override
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT,
method = RequestMethod.DELETE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public EntityProcessingReport hardDelete(@PathVariable final String modelId) {
return this.entityDAO.byModelId(modelId)
.flatMap(this::checkWriteAccess)
.flatMap(entity -> this.examConfigUpdateService.processExamConfigurationMappingChange(
entity,
this::bulkDelete))
.flatMap(this::notifyDeleted)
.flatMap(pair -> this.logBulkAction(pair.b))
.getOrThrow();
}
@Override
protected Result<ExamConfigurationMap> notifyCreated(final ExamConfigurationMap entity) {
// update the attached configurations state to "In Use"
return this.configurationNodeDAO.save(new ConfigurationNode(
entity.configurationNodeId,
null,
null,
null,
null,
null,
null,
ConfigurationStatus.IN_USE))
.map(id -> entity);
}
@Override
protected Result<Pair<ExamConfigurationMap, EntityProcessingReport>> notifyDeleted(
final Pair<ExamConfigurationMap, EntityProcessingReport> pair) {
// update the attached configurations state to "Ready"
return this.configurationNodeDAO.save(new ConfigurationNode(
pair.a.configurationNodeId,
null,
null,
null,
null,
null,
null,
ConfigurationStatus.READY_TO_USE))
.map(id -> pair);
}
private ExamConfigurationMap checkPasswordMatch(final ExamConfigurationMap entity) {
if (entity.hasEncryptionSecret() && !entity.encryptSecret.equals(entity.confirmEncryptSecret)) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.EXAM_CONFIGURATION_MAP.TYPE_NAME,
PasswordChange.ATTR_NAME_PASSWORD,
"examConfigMapping:confirm_encrypt_secret:password.mismatch")));
}
return entity;
}
private ExamConfigurationMap checkConfigurationState(final ExamConfigurationMap entity) {
final ConfigurationStatus status;
if (entity.getConfigStatus() != null) {
status = entity.getConfigStatus();
} else {
status = this.configurationNodeDAO.byPK(entity.configurationNodeId)
.getOrThrow()
.getStatus();
}
if (status != ConfigurationStatus.READY_TO_USE) {
throw new APIMessageException(ErrorMessage.INTEGRITY_VALIDATION.of(
"Illegal SEB Exam Configuration state"));
}
return entity;
}
private ExamConfigurationMap checkNoActiveClientConnections(final ExamConfigurationMap entity) {
if (this.examSessionService.hasActiveSebClientConnections(entity.examId)) {
throw new APIMessageException(ErrorMessage.INTEGRITY_VALIDATION.of(
"The Exam is currently running and has active SEB Client connections"));
}
return entity;
}
}
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
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.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
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.user.PasswordChange;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Pair;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfigurationMapRecordDynamicSqlSupport;
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.bulkaction.BulkActionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile
@RestController
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_CONFIGURATION_MAP_ENDPOINT)
public class ExamConfigurationMappingController extends EntityController<ExamConfigurationMap, ExamConfigurationMap> {
private final ExamDAO examDao;
private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigUpdateService examConfigUpdateService;
private final ExamSessionService examSessionService;
protected ExamConfigurationMappingController(
final AuthorizationService authorization,
final BulkActionService bulkActionService,
final EntityDAO<ExamConfigurationMap, ExamConfigurationMap> entityDAO,
final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ExamDAO examDao,
final ConfigurationNodeDAO configurationNodeDAO,
final ExamConfigUpdateService examConfigUpdateService,
final ExamSessionService examSessionService) {
super(
authorization,
bulkActionService,
entityDAO,
userActivityLogDAO,
paginationService,
beanValidationService);
this.examDao = examDao;
this.configurationNodeDAO = configurationNodeDAO;
this.examConfigUpdateService = examConfigUpdateService;
this.examSessionService = examSessionService;
}
@Override
protected ExamConfigurationMap createNew(final POSTMapper postParams) {
final Long institutionId = postParams.getLong(API.PARAM_INSTITUTION_ID);
return new ExamConfigurationMap(institutionId, postParams);
}
@Override
protected SqlTable getSQLTableOfEntity() {
return ExamConfigurationMapRecordDynamicSqlSupport.examConfigurationMapRecord;
}
@Override
protected EntityType getGrantEntityType() {
return EntityType.EXAM;
}
@Override
protected GrantEntity toGrantEntity(final ExamConfigurationMap entity) {
if (entity == null) {
return null;
}
return this.examDao
.byPK(entity.examId)
.getOrThrow();
}
@Override
protected Result<ExamConfigurationMap> checkCreateAccess(final ExamConfigurationMap entity) {
final GrantEntity grantEntity = toGrantEntity(entity);
this.authorization.checkWrite(grantEntity);
return Result.of(entity);
}
@Override
protected Result<ExamConfigurationMap> validForCreate(final ExamConfigurationMap entity) {
return super.validForCreate(entity)
.map(this::checkConfigurationState)
.map(this::checkPasswordMatch)
.map(this::checkNoActiveClientConnections);
}
@Override
@RequestMapping(
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ExamConfigurationMap create(
@RequestParam final MultiValueMap<String, String> allRequestParams,
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
// check modify privilege for requested institution and concrete entityType
this.checkModifyPrivilege(institutionId);
final POSTMapper postMap = new POSTMapper(allRequestParams)
.putIfAbsent(API.PARAM_INSTITUTION_ID, String.valueOf(institutionId));
final ExamConfigurationMap requestModel = this.createNew(postMap);
return this.checkCreateAccess(requestModel)
.flatMap(this::validForCreate)
.map(this::checkPasswordMatch)
.flatMap(entity -> this.examConfigUpdateService.processExamConfigurationMappingChange(
entity,
this.entityDAO::createNew))
.flatMap(this::logCreate)
.flatMap(this::notifyCreated)
.getOrThrow();
}
@Override
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT,
method = RequestMethod.DELETE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public EntityProcessingReport hardDelete(@PathVariable final String modelId) {
return this.entityDAO.byModelId(modelId)
.flatMap(this::checkWriteAccess)
.flatMap(entity -> this.examConfigUpdateService.processExamConfigurationMappingChange(
entity,
this::bulkDelete))
.flatMap(this::notifyDeleted)
.flatMap(pair -> this.logBulkAction(pair.b))
.getOrThrow();
}
@Override
protected Result<ExamConfigurationMap> notifyCreated(final ExamConfigurationMap entity) {
// update the attached configurations state to "In Use"
return this.configurationNodeDAO.save(new ConfigurationNode(
entity.configurationNodeId,
null,
null,
null,
null,
null,
null,
ConfigurationStatus.IN_USE))
.map(id -> entity);
}
@Override
protected Result<Pair<ExamConfigurationMap, EntityProcessingReport>> notifyDeleted(
final Pair<ExamConfigurationMap, EntityProcessingReport> pair) {
// update the attached configurations state to "Ready"
return this.configurationNodeDAO.save(new ConfigurationNode(
pair.a.configurationNodeId,
null,
null,
null,
null,
null,
null,
ConfigurationStatus.READY_TO_USE))
.map(id -> pair);
}
private ExamConfigurationMap checkPasswordMatch(final ExamConfigurationMap entity) {
if (entity.hasEncryptionSecret() && !entity.encryptSecret.equals(entity.confirmEncryptSecret)) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.EXAM_CONFIGURATION_MAP.TYPE_NAME,
PasswordChange.ATTR_NAME_PASSWORD,
"examConfigMapping:confirm_encrypt_secret:password.mismatch")));
}
return entity;
}
private ExamConfigurationMap checkConfigurationState(final ExamConfigurationMap entity) {
final ConfigurationStatus status;
if (entity.getConfigStatus() != null) {
status = entity.getConfigStatus();
} else {
status = this.configurationNodeDAO.byPK(entity.configurationNodeId)
.getOrThrow()
.getStatus();
}
if (status != ConfigurationStatus.READY_TO_USE) {
throw new APIMessageException(ErrorMessage.INTEGRITY_VALIDATION.of(
"Illegal SEB Exam Configuration state"));
}
return entity;
}
private ExamConfigurationMap checkNoActiveClientConnections(final ExamConfigurationMap entity) {
if (this.examSessionService.hasActiveSebClientConnections(entity.examId)) {
throw new APIMessageException(ErrorMessage.INTEGRITY_VALIDATION.of(
"The Exam is currently running and has active SEB Client connections"));
}
return entity;
}
}

View file

@ -9,7 +9,7 @@ sebserver.overall.about.markup=<span style='font-family: Arial, Helvetica,sans-s
sebserver.overall.help=Documentation
sebserver.overall.help.link=https://www.safeexambrowser.org/news_en.html
sebserver.overall.message.leave.without.save=You have unsaved changes!</br>Are you sure you want to leave the page? The changes will be lost.
sebserver.overall.message.leave.without.save=You have unsaved changes!<br/>Are you sure you want to leave the page? The changes will be lost.
sebserver.overall.upload=Please select a file
sebserver.overall.upload.unsupported.file=This file type is not supported. Supported files are: {0}
sebserver.overall.action.modify.cancel=Cancel
@ -77,6 +77,7 @@ sebserver.form.validation.fieldError.size=The size must be between {3} and {4}
sebserver.form.validation.fieldError.name=The Name is mandatory and must have a size between {3} and {4} character
sebserver.form.validation.fieldError.urlSuffix=The URL Suffix must have a size between {3} and {4} character
sebserver.form.validation.fieldError.notNull=This field is mandatory
sebserver.form.validation.fieldError.name.notunique=This name is already in use. Please choose another one.
sebserver.form.validation.fieldError.username.notunique=This Username is already in use. Please choose another one.
sebserver.form.validation.fieldError.password.wrong=The old password is wrong
sebserver.form.validation.fieldError.password.mismatch=The re-typed password doesn't match the new password
@ -112,7 +113,7 @@ sebserver.login.failed.title=Login failed
sebserver.login.failed.message=Access denied: wrong username or password
sebserver.logout=Sign out
sebserver.logout.success.message=You have been successfully signed out.
sebserver.logout.invalid-session.message=You have been signed out because of a user session invalidation.</br>Please sign in again
sebserver.logout.invalid-session.message=You have been signed out because of a user session invalidation.<br/>Please sign in again
sebserver.login.password.change=Information
sebserver.login.password.change.success=The password was successfully changed. Please sign in with your new password
@ -139,11 +140,11 @@ sebserver.institution.list.actions=
sebserver.institution.list.empty=No institution has been found. Please adapt the filter or create a new institution
sebserver.institution.list.title=Institutions
sebserver.institution.list.column.name=Name
sebserver.institution.list.column.name.tooltip=The name of the institution.</br></br>Use the filter above to narrow down a specific name.</br>{0}
sebserver.institution.list.column.name.tooltip=The name of the institution.<br/><br/>Use the filter above to narrow down a specific name.<br/>{0}
sebserver.institution.list.column.urlSuffix=URL Suffix
sebserver.institution.list.column.urlSuffix.tooltip=The URL suffix to the institutional login page.</br></br>Use the filter above to narrow down a specific URL suffix.</br>{0}
sebserver.institution.list.column.urlSuffix.tooltip=The URL suffix to the institutional login page.<br/><br/>Use the filter above to narrow down a specific URL suffix.<br/>{0}
sebserver.institution.list.column.active=Status
sebserver.institution.list.column.active.tooltip=The activity of the institution.</br></br>Use the filter above to specify the activity.</br>{0}
sebserver.institution.list.column.active.tooltip=The activity of the institution.<br/><br/>Use the filter above to specify the activity.<br/>{0}
sebserver.institution.action.list=Institution
sebserver.institution.action.form=Institution
@ -164,9 +165,9 @@ sebserver.institution.form.title=Institution
sebserver.institution.form.name=Name
sebserver.institution.form.name.tooltip=The name of the institution
sebserver.institution.form.urlSuffix=URL Suffix
sebserver.institution.form.urlSuffix.tooltip=The URL suffix to the institutional login page.</br> Institutional URL is: http(s)://<seb-server-name>/<suffix>
sebserver.institution.form.urlSuffix.tooltip=The URL suffix to the institutional login page.<br/> Institutional URL is: http(s)://<seb-server-name>/<suffix>
sebserver.institution.form.logoImage=Logo Image
sebserver.institution.form.logoImage.tooltip=The Image that is shown as a logo in the specified institutional login page.</br>In edit mode, use the arrow sign to open a upload dialog.
sebserver.institution.form.logoImage.tooltip=The Image that is shown as a logo in the specified institutional login page.<br/>In edit mode, use the arrow sign to open a upload dialog.
sebserver.institution.form.logoImage.unsupportedFileType=The selected file is not supported. Supported are: PNG and JPG
@ -176,27 +177,27 @@ sebserver.institution.form.logoImage.unsupportedFileType=The selected file is no
sebserver.useraccount.list.actions=
sebserver.useraccount.role.SEB_SERVER_ADMIN=SEB Server Administrator
sebserver.useraccount.role.SEB_SERVER_ADMIN.tooltip=A SEB server administrator has overall read privileges</br>and is able to create new institutions and user accounts
sebserver.useraccount.role.SEB_SERVER_ADMIN.tooltip=A SEB server administrator has overall read privileges<br/>and is able to create new institutions and user accounts
sebserver.useraccount.role.INSTITUTIONAL_ADMIN=Institutional Administrator
sebserver.useraccount.role.INSTITUTIONAL_ADMIN.tooltip=An institutional administrator has overall read privileges on the assigned institution</br>and is able to edit the institution and create new user accounts for the institution.</br>Furthermore an institutional administrator is able to create new LMS bindings and SEB client configurations for the institution.
sebserver.useraccount.role.INSTITUTIONAL_ADMIN.tooltip=An institutional administrator has overall read privileges on the assigned institution<br/>and is able to edit the institution and create new user accounts for the institution.<br/>Furthermore an institutional administrator is able to create new LMS bindings and SEB client configurations for the institution.
sebserver.useraccount.role.EXAM_ADMIN=Exam Administrator
sebserver.useraccount.role.EXAM_ADMIN.tooltip=An exam administrator has overall read privileges for the institution but cannot see or manage other user accounts.</br>An exam administrator is able to create new SEB configurations and import and setup exams.
sebserver.useraccount.role.EXAM_ADMIN.tooltip=An exam administrator has overall read privileges for the institution but cannot see or manage other user accounts.<br/>An exam administrator is able to create new SEB configurations and import and setup exams.
sebserver.useraccount.role.EXAM_SUPPORTER=Exam Supporter
sebserver.useraccount.role.EXAM_SUPPORTER.tooltip=An exam supporter can only see and edit the own user account</br> and monitor exams for that he/she was attached by an exam administrator.
sebserver.useraccount.role.EXAM_SUPPORTER.tooltip=An exam supporter can only see and edit the own user account<br/> and monitor exams for that he/she was attached by an exam administrator.
sebserver.useraccount.list.empty=No user account has been found. Please adapt the filter or create a new user account
sebserver.useraccount.list.title=User Accounts
sebserver.useraccount.list.column.institution=Institution
sebserver.useraccount.list.column.institution.tooltip=The institution of the user account.</br></br>Use the filter above to specify the institution.</br>{0}
sebserver.useraccount.list.column.institution.tooltip=The institution of the user account.<br/><br/>Use the filter above to specify the institution.<br/>{0}
sebserver.useraccount.list.column.name=First Name
sebserver.useraccount.list.column.name.tooltip=The first name of the user.</br></br>Use the filter above to narrow down a specific first name.</br>{0}
sebserver.useraccount.list.column.name.tooltip=The first name of the user.<br/><br/>Use the filter above to narrow down a specific first name.<br/>{0}
sebserver.useraccount.list.column.username=User Name
sebserver.useraccount.list.column.username.tooltip=The internal user name of the user.</br></br>Use the filter above to narrow down a specific user name.</br>{0}
sebserver.useraccount.list.column.username.tooltip=The internal user name of the user.<br/><br/>Use the filter above to narrow down a specific user name.<br/>{0}
sebserver.useraccount.list.column.email=Mail
sebserver.useraccount.list.column.email.tooltip=The e-mail address of the user.</br></br>Use the filter above to narrow down a specific e-mail address.</br>{0}
sebserver.useraccount.list.column.email.tooltip=The e-mail address of the user.<br/><br/>Use the filter above to narrow down a specific e-mail address.<br/>{0}
sebserver.useraccount.list.column.language=Language
sebserver.useraccount.list.column.active=Active
sebserver.useraccount.list.column.active.tooltip=The activity of the user.</br></br>Use the filter above to specify the activity.</br>{0}
sebserver.useraccount.list.column.active.tooltip=The activity of the user.<br/><br/>Use the filter above to specify the activity.<br/>{0}
sebserver.useraccount.action.list=User Account
sebserver.useraccount.action.form=User Account of {0}
@ -231,9 +232,9 @@ sebserver.useraccount.form.mail.tooltip=The e-mail address of the user.
sebserver.useraccount.form.language=Language
sebserver.useraccount.form.language.tooltip=The users language.
sebserver.useraccount.form.timezone=Time Zone
sebserver.useraccount.form.timezone.tooltip=The time-zone of the user.</br></br>Note that this also defines the exact time and date that is displayed to the user.
sebserver.useraccount.form.timezone.tooltip=The time-zone of the user.<br/><br/>Note that this also defines the exact time and date that is displayed to the user.
sebserver.useraccount.form.roles=User Roles
sebserver.useraccount.form.roles.tooltip=Select the roles for the user.</br>A user can have more then one role but must have at least one.</br></br>Please use the tooltip on the role name for more information about a specific role.
sebserver.useraccount.form.roles.tooltip=Select the roles for the user.<br/>A user can have more then one role but must have at least one.<br/><br/>Please use the tooltip on the role name for more information about a specific role.
sebserver.useraccount.form.password=Password
sebserver.useraccount.form.password.tooltip=The password of the user account
sebserver.useraccount.form.password.confirm=Confirm Password
@ -257,9 +258,13 @@ sebserver.lmssetup.list.action.no.modify.privilege=No Access: A LMS Setup from o
sebserver.lmssetup.list.empty=No LMS Setup has been found. Please adapt the filter or create a new LMS Setup
sebserver.lmssetup.list.title=Learning Management System Setups
sebserver.lmssetup.list.column.institution=Institution
sebserver.lmssetup.list.column.institution.tooltip=The institution of the LMS setup.<br/><br/>Use the filter above to specify the institution.<br/>{0}
sebserver.lmssetup.list.column.name=Name
sebserver.lmssetup.list.column.name.tooltip=The name of the LMS setup.<br/><br/>Use the filter above to narrow down a specific LMS by name.<br/>{0}
sebserver.lmssetup.list.column.type=LMS Type
sebserver.lmssetup.list.column.type.tooltip=The type of the LMS.<br/><br/>Use the filter above to specify the LMS type.<br/>{0}
sebserver.lmssetup.list.column.active=Active
sebserver.lmssetup.list.column.active.tooltip=The activity of the LMS Setup.<br/><br/>Use the filter above to specify the activity.<br/>{0}
sebserver.lmssetup.action.list=LMS Connection Settings
sebserver.lmssetup.action.form=LMS Setup
@ -270,7 +275,7 @@ sebserver.lmssetup.action.modify=Edit
sebserver.lmssetup.action.savetest=Test And Save
sebserver.lmssetup.action.testsave=Test And Save
sebserver.lmssetup.action.test.ok=Successfully connected to the course API
sebserver.lmssetup.action.test.tokenRequestError=The API access was denied: {0}
sebserver.lmssetup.action.test.tokenRequestError=The API access was denied: {0}<br/>Please check the LMS connection details.
sebserver.lmssetup.action.test.quizRequestError=Unable to request courses or quizzes from the course API of the LMS. {0}
sebserver.lmssetup.action.test.quizRestrictionError=Unable to access course restriction API of the LMS. {0}
sebserver.lmssetup.action.test.missingParameter=There is one or more missing connection parameter.<br/>Please check the connection parameter for this LMS Setup
@ -285,17 +290,25 @@ sebserver.lmssetup.info.pleaseSelect=Please select first a LMS Setup from the li
sebserver.lmssetup.form.title=Learning Management System Setup
sebserver.lmssetup.form.title.new=Add Learning Management System Setup
sebserver.lmssetup.form.institution=Institution
sebserver.lmssetup.form.institution.tooltip=The institution where the LMS setup belongs to
sebserver.lmssetup.form.name=Name
sebserver.lmssetup.form.name.tooltip=The name of the LMS setup
sebserver.lmssetup.form.type=Type
sebserver.lmssetup.form.clientname.seb=SEB Auth. Name
sebserver.lmssetup.form.secret.seb=SEB Auth. Password
sebserver.lmssetup.form.type.tooltip=The type of the LMS Setup.
sebserver.lmssetup.form.url=LMS Server Address
sebserver.lmssetup.form.url.tooltip=The server URL to the specific LMS server.<br/><br/>This should point to the main- or root-address of the LMS server
sebserver.lmssetup.form.clientname.lms=LMS Server Username
sebserver.lmssetup.form.clientname.lms.tooltip=The client name of the API client access to the LMS.<br/><br/>This is usually provided by an LMS administrator that has created a API access account for SEB Server binding within the LMS server.
sebserver.lmssetup.form.secret.lms=LMS Server Password
sebserver.lmssetup.form.secret.lms.tooltip=The secret or password of the API client access to the LMS.<br/><br/>This is usually provided by an LMS administrator that has created a API access account for SEB Server binding within the LMS server.
sebserver.lmssetup.form.proxy=Proxy
sebserver.lmssetup.form.proxy.tooltip=The proxy details of a proxy is needed to connect to a LMS server
sebserver.lmssetup.form.proxy.host=Proxy Host
sebserver.lmssetup.form.proxy.host.tooltip=The server name of the proxy host to connect to
sebserver.lmssetup.form.proxy.port=Proxy Port
sebserver.lmssetup.form.proxy.port.tooltip=The proxy server port to connect to
sebserver.lmssetup.form.proxy.auth-credentials=Proxy Name/Password
sebserver.lmssetup.form.proxy.auth-credentials.tooltip=The proxy authentication credentials (name and password)<br/>to authenticate the connection within the proxy server
################################
# Quiz Discovery
@ -306,10 +319,15 @@ sebserver.quizdiscovery.list.actions=
sebserver.quizdiscovery.list.title=Quizzes
sebserver.quizdiscovery.list.empty=No Quiz has been found. Please adapt the filter or create a new LMS Setup
sebserver.quizdiscovery.list.column.institution=Institution
sebserver.quizdiscovery.list.column.institution.tooltip=The institution of the LMS setup that defines LMS of the quiz.<br/><br/>Use the filter above to specify the institution.<br/>{0}
sebserver.quizdiscovery.list.column.lmssetup=LMS
sebserver.quizdiscovery.list.column.lmssetup.tooltip=The LMS setup that defines the LMS of the quiz<br/><br/>Use the filter above to specify the LMS setup.<br/>{0}
sebserver.quizdiscovery.list.column.name=Name
sebserver.quizdiscovery.list.column.name.tooltip=The name of the quiz.<br/><br/>Use the filter above to narrow down a specific quiz name.<br/>{0}
sebserver.quizdiscovery.list.column.starttime=Start Time {0}
sebserver.quizdiscovery.list.column.starttime.tooltip=The start time of the quiz.<br/><br/>Use the filter above to set a specific from date.<br/>{0}
sebserver.quizdiscovery.list.column.endtime=End Time {0}
sebserver.quizdiscovery.list.column.endtime.tooltip=The end time of the quiz.<br/><br/>{0}
sebserver.quizdiscovery.info.pleaseSelect=Please select first a Quiz from the list
sebserver.quizdiscovery.action.list=LMS Exam Discovery
@ -318,16 +336,32 @@ sebserver.quizdiscovery.quiz.import.out.dated=The Selected Quiz is is already fi
sebserver.quizdiscovery.action.details=Show Details
sebserver.quizdiscovery.quiz.details.title=Quiz Details
sebserver.quizdiscovery.quiz.details.institution=Institution
sebserver.quizdiscovery.quiz.details.institution.tooltip=The institution of the LMS setup that defines the LMS of the quiz.
sebserver.quizdiscovery.quiz.details.lmssetup=LMS Setup
sebserver.quizdiscovery.quiz.details.lmssetup.tooltip=The LMS setup that defines the LMS of the quiz.
sebserver.quizdiscovery.quiz.details.name=Name
sebserver.quizdiscovery.quiz.details.name.tooltip=The name of the quiz.<br/><br/>This name is defined on the corresponding LMS
sebserver.quizdiscovery.quiz.details.description=Description
sebserver.quizdiscovery.quiz.details.description.tooltip=The description of the quiz.<br/><br/>This description is defined on the corresponding LMS
sebserver.quizdiscovery.quiz.details.starttime=Start Time
sebserver.quizdiscovery.quiz.details.starttime.tooltip=The start time of the quiz.<br/><br/>This time is set on the corresponding LMS
sebserver.quizdiscovery.quiz.details.endtime=End Time
sebserver.quizdiscovery.quiz.details.endtime.tooltip=The end time of the quiz.<br/><br/>This time is set on the corresponding LMS
sebserver.quizdiscovery.quiz.details.url=Start URL
sebserver.quizdiscovery.quiz.details.url.tooltip=The start URL on the LMS for the quiz.<br/><br/>This is defined by the LMS setup and the quiz URL
sebserver.quizdiscovery.quiz.details.additional.timecreated=Creation Time
sebserver.quizdiscovery.quiz.details.additional.timecreated.tooltip=The time when the quiz was first created<br/><br/>This time is defined by the corresponding LMS
sebserver.quizdiscovery.quiz.details.additional.course_shortname=Short Name
sebserver.quizdiscovery.quiz.details.additional.course_shortname.tooltip=The short name of the quiz<br/><br/>This is defined on the corresponding LMS
sebserver.quizdiscovery.quiz.details.additional.course_fullname=Full Name
sebserver.quizdiscovery.quiz.details.additional.course_fullname.tooltip=The full name of the quiz<br/><br/>This is defined on the corresponding LMS
sebserver.quizdiscovery.quiz.details.additional.course_displayname=Display Name
sebserver.quizdiscovery.quiz.details.additional.course_displayname.tooltip=The display name of the quiz<br/><br/>This is defined on the corresponding LMS
sebserver.quizdiscovery.quiz.details.additional.course_summary=Summary
sebserver.quizdiscovery.quiz.details.additional.course_summary.tooltip=The summary of the quiz<br/><br/>This is defined on the corresponding LMS
sebserver.quizdiscovery.quiz.details.additional.timelimit=Time Limit
sebserver.quizdiscovery.quiz.details.additional.timelimit.toolitp=The time limit of the quiz<br/><br/>This is defined on the corresponding LMS
################################
# Exam
@ -336,10 +370,15 @@ sebserver.quizdiscovery.quiz.details.additional.timelimit=Time Limit
sebserver.exam.list.actions=
sebserver.exam.list.title=Exams
sebserver.exam.list.column.institution=Institution
sebserver.exam.list.column.institution.tooltip=The institution of the LMS setup that defines LMS of the exam.<br/><br/>Use the filter above to specify the institution.<br/>{0}
sebserver.exam.list.column.lmssetup=LMS
sebserver.exam.list.column.lmssetup.tooltip=The LMS setup that defines the LMS of the exam<br/><br/>Use the filter above to specify the LMS setup.<br/>{0}
sebserver.exam.list.column.name=Name
sebserver.exam.list.column.name.tooltip=The name of the exam.<br/><br/>Use the filter above to narrow down a specific exam name.<br/>{0}
sebserver.exam.list.column.starttime=Start Time {0}
sebserver.exam.list.column.starttime.tooltip=The start time of the exam.<br/><br/>Use the filter above to set a specific from date.<br/>{0}
sebserver.exam.list.column.type=Type
sebserver.exam.list.column.type.tooltip=The type of the exam.<br/><br/>Use the filter above to set a specific exam type.<br/>{0}
sebserver.exam.list.empty=No Exam has been found. Please adapt the filter or import one from Quiz
sebserver.exam.list.modify.out.dated=Finished exams cannot be modified.
@ -369,43 +408,73 @@ sebserver.exam.info.pleaseSelect=Please select first an Exam from the list
sebserver.exam.form.title.import=Import Exam
sebserver.exam.form.title=Exam
sebserver.exam.form.lmssetup=LMS Setup
sebserver.exam.form.lmssetup.tooltip=The LMS setup that defines the LMS of the exam.
sebserver.exam.form.quizid=Quiz Identifier
sebserver.exam.form.quizid.tooltip=The identifier that identifies the quiz of the exam on the corresponding LMS
sebserver.exam.form.quizurl=Quiz URL
sebserver.exam.form.quizurl.tooltip=The direct URL link to the quiz/exam on the LMS
sebserver.exam.form.name=Name
sebserver.exam.form.name.tooltip=The name of the exam.<br/><br/>This name is defined on the corresponding LMS
sebserver.exam.form.description=Description
sebserver.exam.form.description.tooltip=The description of the exam.<br/><br/>This description is defined on the corresponding LMS
sebserver.exam.form.starttime=Start Time
sebserver.exam.form.starttime.tooltip=The start time of the exam.<br/><br/>This time is set on the corresponding LMS
sebserver.exam.form.endtime=End Time
sebserver.exam.form.endtime.tooltip=The end time of the exam.<br/><br/>This time is set on the corresponding LMS
sebserver.exam.form.status=Status
sebserver.exam.form.status.tooltip=The current status for the exam.<br/><br/>Either "Up Coming" for an exam that has not yet been started,<br/>"Running" for an exam that is currently running<br/>or "Finished" for an exam that has already been finished yet
sebserver.exam.form.type=Exam Type
sebserver.exam.form.type.tooltip=The type of the exam.<br/><br/>This has only descriptive character for now and can be used to categorise exams within a type
sebserver.exam.form.supporter=Exam Supporter
sebserver.exam.form.supporter.action.add=Add as supporter for this exam
sebserver.exam.form.supporter.action.remove=Remove supporter
sebserver.exam.form.supporter.tooltip=A list of users that are allowed to support this exam.<br/><br/>To add a user in edit mode click into the field on right-hand and start typing the first letters of the username.<br/>A filtered choice will drop down. Click on a specific username on the drop-down to add the user to the list.<br/>To remove a user from the list, just double-click the username on the list.
sebserver.exam.form.sebrestriction.title=SEB Restriction Details
sebserver.exam.form.sebrestriction.info=Info
sebserver.exam.form.sebrestriction.info-text=A detailed description of the SEB restriction can be found within this page:<br/><a href="https://seb-openedx.readthedocs.io/en/latest/usage.html">https://seb-openedx.readthedocs.io/en/latest/usage.html</a>
sebserver.exam.form.sebrestriction.configKeys=Config Keys
sebserver.exam.form.sebrestriction.configKeys.tooltip=A comma-separated list of SEB Config Keys that are automatically generated from the attached SEB exam configurations<br/>and are checked by the LMS for the restricted SEB access for every request
sebserver.exam.form.sebrestriction.browserExamKeys=Browser Exam Keys
sebserver.exam.form.sebrestriction.browserExamKeys.tooltip=A comma-separated list of SEB Browser Exam Keys<br/>that are checked by the LMS for the restricted SEB access for every request
sebserver.exam.form.sebrestriction.WHITELIST_PATHS=Component White-List
sebserver.exam.form.sebrestriction.WHITELIST_PATHS.tooltip=Grant no-restriction to each of the given Open edX path components by select them for white-list.
sebserver.exam.form.sebrestriction.BLACKLIST_CHAPTERS=Chapters Black-List
sebserver.exam.form.sebrestriction.BLACKLIST_CHAPTERS.tooltip=Explicitly restrict a course chapter by adding the course-chapter-identifier to this comma-separated list
sebserver.exam.form.sebrestriction.PERMISSION_COMPONENTS=Permissions
sebserver.exam.form.sebrestriction.PERMISSION_COMPONENTS.tooltip=Define the additional SEB restriction permissions
sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED=User Banning
sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED.tooltip=Indicates whether the user of a restricted access shall be banned on authentication failure or not.
sebserver.exam.form.sebrestriction.whiteListPaths.ABOUT=About
sebserver.exam.form.sebrestriction.whiteListPaths.ABOUT.tooltip=The "About" section of the Open edX course
sebserver.exam.form.sebrestriction.whiteListPaths.COURSE_OUTLINE=Course Outline
sebserver.exam.form.sebrestriction.whiteListPaths.COURSE_OUTLINE.tooltip=The outline section of the Open edX course
sebserver.exam.form.sebrestriction.whiteListPaths.COURSE_WARE=Course Ware
sebserver.exam.form.sebrestriction.whiteListPaths.COURSE_WARE.tooltip=The actual course and course content
sebserver.exam.form.sebrestriction.whiteListPaths.DISCUSSION=Discussion
sebserver.exam.form.sebrestriction.whiteListPaths.DISCUSSION.tooltip=The discussion section of the Open edX course
sebserver.exam.form.sebrestriction.whiteListPaths.PROGRESS=Progress
sebserver.exam.form.sebrestriction.whiteListPaths.PROGRESS.tooltip=The progress overview of the Open edX course
sebserver.exam.form.sebrestriction.whiteListPaths.WIKI=Description (Wiki)
sebserver.exam.form.sebrestriction.whiteListPaths.WIKI.tooltip=The wikipedia section of the Open edX course
sebserver.exam.form.sebrestriction.permissions.ALWAYS_ALLOW_STUFF=Stuff Role Always Allowed
sebserver.exam.form.sebrestriction.permissions.ALWAYS_ALLOW_STUFF.tooltip=Set this to always allow none-restricted access for a user that has "stuff" privileges.
sebserver.exam.form.sebrestriction.permissions.CHECK_BROWSER_EXAM_KEY=Check Browser-Exam-Key
sebserver.exam.form.sebrestriction.permissions.CHECK_BROWSER_EXAM_KEY.tooltip=Always check received SEB Browser Exam Key with the defined ones for every request.
sebserver.exam.form.sebrestriction.permissions.CHECK_CONFIG_KEY=Check Config-Key
sebserver.exam.form.sebrestriction.permissions.CHECK_CONFIG_KEY.tooltip=Always check received SEB Config Key with the defined ones for every request.
sebserver.exam.form.sebrestriction.permissions.CHECK_BROWSER_EXAM_OR_CONFIG_KEY=Check Browser-Exam- Or Config-Key
sebserver.exam.form.sebrestriction.permissions.CHECK_BROWSER_EXAM_OR_CONFIG_KEY.tooltip=Always check either SEB Browser Exam Key or SEB Config Key with the defined ones for every request.
sebserver.exam.type.UNDEFINED=Not Defined
sebserver.exam.type.UNDEFINED.tooltip=No exam type specified
sebserver.exam.type.MANAGED=Managed Devices
sebserver.exam.type.MANAGED.tooltip=Exam type specified for managed devices
sebserver.exam.type.BYOD=Bring Your Own Device
sebserver.exam.type.BYOD.tooltip=Exam type specified for bring your own devices
sebserver.exam.type.VDI=VDI (Virtual Desktop Infrastructure)
sebserver.exam.type.VDI.tooltip=Exam type specified for Virtual Desktop Infrastructure
sebserver.exam.status.UP_COMING=Up Coming
sebserver.exam.status.RUNNING=Running
@ -413,9 +482,13 @@ sebserver.exam.status.FINISHED=Finished
sebserver.exam.configuration.list.actions=
sebserver.exam.configuration.list.title=SEB Exam Configuration
sebserver.exam.configuration.list.title.tooltip=A list of all attached SEB exam configuration for this exam.
sebserver.exam.configuration.list.column.name=Name
sebserver.exam.configuration.list.column.name.tooltip=The name of the attached SEB exam configuration.
sebserver.exam.configuration.list.column.description=Description
sebserver.exam.configuration.list.column.description.tooltip=The description of the attached SEB exam configuration.
sebserver.exam.configuration.list.column.status=Status
sebserver.exam.configuration.list.column.status.tooltip=The current status of the attached SEB exam configuration.
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 first a SEB Configuration from the list
sebserver.exam.configuration.action.noconfig.message=There is currently no SEB exam configuration to select.<br/>Please create one in SEB Configuration / Exam Configuration
@ -431,25 +504,34 @@ sebserver.exam.configuration.action.get-config-key=Export Config-Key
sebserver.exam.configuration.form.title.new=Add SEB Configuration Mapping
sebserver.exam.configuration.form.title=SEB Configuration Mapping
sebserver.exam.configuration.form.name=SEB Configuration
sebserver.exam.configuration.form.name.tooltip=Please select a SEB exam configuration to attach to the exam
sebserver.exam.configuration.form.encryptSecret=Encryption Password
sebserver.exam.configuration.form.encryptSecret.tooltip=Define a encryption password if the SEB exam configuration should be encrypted by password
sebserver.exam.configuration.form.description=Description
sebserver.exam.configuration.form.description.tooltip=The description of the selected SEB exam configuration
sebserver.exam.configuration.form.status=Status
sebserver.exam.configuration.form.status.tooltip=The current status of the selected SEB exam configuration
sebserver.exam.configuration.form.encryptSecret.confirm=Confirm Password
sebserver.exam.configuration.form.encryptSecret.confirm.tooltip=Please confirm the encryption password if there is one
sebserver.exam.indicator.list.actions=
sebserver.exam.indicator.list.title=Indicators
sebserver.exam.indicator.list.title.tooltip=A list of indicators that are shown on the exam monitoring view
sebserver.exam.indicator.list.column.type=Type
sebserver.exam.indicator.list.column.type.tooltip=The type of indicator
sebserver.exam.indicator.list.column.name=Name
sebserver.exam.indicator.list.column.name.tooltip=The name of the indicator
sebserver.exam.indicator.list.column.thresholds=Thresholds
sebserver.exam.indicator.list.column.thresholds.tooltip=The thresholds of the indicator
sebserver.exam.indicator.list.empty=There is currently no indicator defined for this exam. Please create a new one
sebserver.exam.indicator.list.pleaseSelect=Please select first an indicator from the list
sebserver.exam.indicator.type.LAST_PING=Last Ping Time
sebserver.exam.indicator.type.ERROR_COUNT=Errors
sebserver.exam.indicator.type.WARN_COUNT=Warnings
sebserver.exam.indicator.type.description.LAST_PING=This indicator shows the time in milliseconds since</br> the last ping has been received from a SEB Client.</br>This indicator can be used to track a SEB Client connection and indicate connection losse.</br></br>Thresholds are defined in milliseconds.
sebserver.exam.indicator.type.description.ERROR_COUNT=This indicator shows the number of error log messages that</br> has been received from a SEB Client.</br>This indicator can be used to track errors of connected SEB Clients</br></br>Thresholds are defined by natural numbers.
sebserver.exam.indicator.type.description.WARN_COUNT=This indicator shows the number of warn log messages that</br> has been received from a SEB Client.</br>This indicator can be used to track warnings of connected SEB Clients</br></br>Thresholds are defined by natural numbers.
sebserver.exam.indicator.type.description.LAST_PING=This indicator shows the time in milliseconds since<br/> the last ping has been received from a SEB Client.<br/>This indicator can be used to track a SEB Client connection and indicate connection loss.<br/><br/>The value is in milliseconds.
sebserver.exam.indicator.type.description.ERROR_COUNT=This indicator shows the number of error log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track errors of connected SEB Clients<br/><br/>The value is natural numbers.
sebserver.exam.indicator.type.description.WARN_COUNT=This indicator shows the number of warn log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track warnings of connected SEB Clients<br/><br/>The value is natural numbers.
sebserver.exam.indicator.info.pleaseSelect=Please select first an indicator from the list
@ -462,19 +544,27 @@ sebserver.exam.indicator.action.save=Save
sebserver.exam.indicator.form.title=Indicator
sebserver.exam.indicator.form.title.new=Add Indicator
sebserver.exam.indicator.form.exam=Exam
sebserver.exam.indicator.form.exam.tooltip=The exam this indicator belongs to.
sebserver.exam.indicator.form.name=Name
sebserver.exam.indicator.form.name.tooltip=The name of the indicator.<br/><br/>This name is also displayed as the column title of the indicator on the exam monitoring view
sebserver.exam.indicator.form.type=Type
sebserver.exam.indicator.form.type.tooltip=The type of the indicator.<br/><br/>There are only a set of defined indicators to choose from.<br/>Choose one to see a detailed description for each indicator below.
sebserver.exam.indicator.form.description=Type Description
sebserver.exam.indicator.form.description.tooltip=A detailed description of the selected indicator.
sebserver.exam.indicator.form.color=Default Color
sebserver.exam.indicator.form.color.tooltip=The default color that is displayed on the exam monitoring for this indicator.
sebserver.exam.indicator.form.color.action=Please select a color
sebserver.exam.indicator.form.thresholds=Thresholds
sebserver.exam.indicator.form.thresholds.tooltip=A list of value / color pairs that defines the thresholds of the indicator.<br/><br/>On the exam monitoring view a cell of the indicator is displayed in the specified color when the defined threshold value is reached
sebserver.exam.indicator.thresholds.select.color=Please select a color
sebserver.exam.indicator.thresholds.list.title=Thresholds
sebserver.exam.indicator.thresholds.list.value=Value
sebserver.exam.indicator.thresholds.list.value.tooltip=The threshold value.
sebserver.exam.indicator.thresholds.list.color=Color
sebserver.exam.indicator.thresholds.list.add=Add Threshold
sebserver.exam.indicator.thresholds.list.remove=Delete Threshold
sebserver.exam.indicator.thresholds.list.color.tooltip=The color that is displayed on the exam monitoring view when indicator value has reached the defined threshold value.
sebserver.exam.indicator.thresholds.list.add=Add a new threshold
sebserver.exam.indicator.thresholds.list.remove=Delete this threshold
################################
# SEB Client Configuration
@ -489,28 +579,40 @@ sebserver.clientconfig.list.empty=There is currently no SEB-Client configuration
sebserver.clientconfig.list.title=SEB Client Configurations
sebserver.clientconfig.list.actions=
sebserver.clientconfig.list.column.institution=Institution
sebserver.clientconfig.list.column.institution.tooltip=The institution of the SEB client configuration.</br></br>Use the filter above to specify the institution.</br>{0}
sebserver.clientconfig.list.column.institution.tooltip=The institution of the SEB client configuration.<br/><br/>Use the filter above to specify the institution.<br/>{0}
sebserver.clientconfig.list.column.name=Name
sebserver.clientconfig.list.column.name.tooltip=The name of the SEB client configuration.</br></br>Use the filter above to narrow down a specific name.</br>{0}
sebserver.clientconfig.list.column.name.tooltip=The name of the SEB client configuration.<br/><br/>Use the filter above to narrow down a specific name.<br/>{0}
sebserver.clientconfig.list.column.date=Creation Date {0}
sebserver.clientconfig.list.column.date.tooltip=The date when the SEB client configuration was first created.</br></br>Use the filter above to specify a from-date.</br>{0}
sebserver.clientconfig.list.column.date.tooltip=The date when the SEB client configuration was first created.<br/><br/>Use the filter above to specify a from-date.<br/>{0}
sebserver.clientconfig.list.column.active=Active
sebserver.clientconfig.list.column.active.tooltip=The activity of SEB client configuration.</br></br>Use the filter above to specify the activity.</br>{0}
sebserver.clientconfig.list.column.active.tooltip=The activity of SEB client configuration.<br/><br/>Use the filter above to specify the activity.<br/>{0}
sebserver.clientconfig.info.pleaseSelect=Please select first a Client Configuration from the list
sebserver.clientconfig.list.action.no.modify.privilege=No Access: A SEB Client Configuration from other institution cannot be modified.
sebserver.clientconfig.form.title.new=Add Client Configuration
sebserver.clientconfig.form.title=SEB Client Configuration
sebserver.clientconfig.form.name=Name
sebserver.clientconfig.form.name.tooltip=The name of the SEB Client Configuration.</br>Can be any name that not already exists for another SEB Client Configuration
sebserver.clientconfig.form.name.tooltip=The name of the SEB Client Configuration.<br/>Can be any name that not already exists for another SEB Client Configuration
sebserver.clientconfig.form.fallback=With Fallback
sebserver.clientconfig.form.fallback.tooltip=Indicates whether this SEB Client Configuration has a fallback definition or not
sebserver.clientconfig.form.fallback-url=Fallback Start URL
sebserver.clientconfig.form.fallback-url.tooltip=A fallback URL that tells the SEB where to go when the SEB Server service is unavailable.
sebserver.clientconfig.form.sebServerFallbackTimeout=Fallback Timeout
sebserver.clientconfig.form.sebServerFallbackTimeout.tooltip=Defines the fallback timeout for the SEB Client in milli-seconds.
sebserver.clientconfig.form.sebServerFallbackAttempts=Fallback Attempts
sebserver.clientconfig.form.sebServerFallbackAttempts.tooltip=The number of connection attempts a SEB Client is trying before switching to fallback case.
sebserver.clientconfig.form.sebServerFallbackAttemptInterval=Attempt Interval
sebserver.clientconfig.form.sebServerFallbackAttemptInterval.tooltip=The interval (in milli-seconds) between connection attempts a SEB Client shall use.
sebserver.clientconfig.form.sebServerFallbackPasswordHash=Fallback Password
sebserver.clientconfig.form.sebServerFallbackPasswordHash.tooltip=A password if set, a SEB Client user must give before the SEB Client starts the fallback procedure.
sebserver.clientconfig.form.date=Creation Date
sebserver.clientconfig.form.date.tooltip=The date when the SEB client configuration was first created.
sebserver.clientconfig.form.encryptSecret=Configuration Password
sebserver.clientconfig.form.encryptSecret.tooltip=Define a password if the SEB client configuration shall be password-encrypted
sebserver.clientconfig.form.encryptSecret.confirm=Confirm Password
sebserver.clientconfig.form.encryptSecret.confirm.tooltip=Please retype the given password for configrmation
sebserver.clientconfig.form.encryptSecret.confirm.tooltip=Please retype the given password for confirmation
sebserver.clientconfig.form.sebConfigPurpose=Configuration Purpose
sebserver.clientconfig.form.sebConfigPurpose.tooltip=This indicates whether this client configuration shall be used to configure the SEB Client or to start an exam
sebserver.clientconfig.action.list.new=Add Configuration
sebserver.clientconfig.action.list.view=View Configuration
@ -527,13 +629,13 @@ sebserver.clientconfig.action.deactivate=Deactivate Configuration
sebserver.examconfig.action.list=Exam Configuration
sebserver.examconfig.list.title=Exam Configurations
sebserver.examconfig.list.column.institution=Institution
sebserver.examconfig.list.column.institution.tooltip=The institution of the SEB exam configuration.</br></br>Use the filter above to specify the institution.</br>{0}
sebserver.examconfig.list.column.institution.tooltip=The institution of the SEB exam configuration.<br/><br/>Use the filter above to specify the institution.<br/>{0}
sebserver.examconfig.list.column.name=Name
sebserver.examconfig.list.column.name.tooltip=The name of the SEB exam configuration.</br></br>Use the filter above to narrow down a specific name.</br>{0}
sebserver.examconfig.list.column.name.tooltip=The name of the SEB exam configuration.<br/><br/>Use the filter above to narrow down a specific name.<br/>{0}
sebserver.examconfig.list.column.description=Description
sebserver.examconfig.list.column.description.tooltip=The description of the SEB exam configuration.</br></br>Use the filter above to find configurations that contains specific words or phrases within the description.</br>{0}
sebserver.examconfig.list.column.description.tooltip=The description of the SEB exam configuration.<br/><br/>Use the filter above to find configurations that contains specific words or phrases within the description.<br/>{0}
sebserver.examconfig.list.column.status=Status
sebserver.examconfig.list.column.status.tooltip=The status of the SEB exam configuration.</br></br>Use the filter above to specify a status.</br>{0}
sebserver.examconfig.list.column.status.tooltip=The status of the SEB exam configuration.<br/><br/>Use the filter above to specify a status.<br/>{0}
sebserver.examconfig.list.actions=
@ -577,10 +679,10 @@ sebserver.examconfig.form.with-history=With History
sebserver.examconfig.form.template=Template
sebserver.examconfig.form.template.tooltip=The template this SEB exam configuration depends on.
sebserver.examconfig.form.status=Status
sebserver.examconfig.form.status.tooltip=The status of this SEB exam configuration.</br></br>Under Construction marks a SEB exam configuration to not be able to attach to an exam so far.</br></br>Ready to use marks an SEB exam configuration to be able to attach to an exam.</br></br>In Use marks a SEB exam configuration is already been used from one or more exam(s)
sebserver.examconfig.form.status.tooltip=The status of this SEB exam configuration.<br/><br/>Under Construction marks a SEB exam configuration to not be able to attach to an exam so far.<br/><br/>Ready to use marks an SEB exam configuration to be able to attach to an exam.<br/><br/>In Use marks a SEB exam configuration is already been used from one or more exam(s)
sebserver.examconfig.form.config-key.title=Config Key
sebserver.examconfig.form.attached-to=Attached To Exam
sebserver.examconfig.form.attached-to.tooltip=This SEB exam configuration is currently attached to the following exams.</br></br>Select an exam from the list and use the "View Exam" or Double-Click on the list to go to a specific exam.
sebserver.examconfig.form.attached-to.tooltip=This SEB exam configuration is currently attached to the following exams.<br/><br/>Select an exam from the list and use the "View Exam" or Double-Click on the list to go to a specific exam.
sebserver.examconfig.status.CONSTRUCTION=Under Construction
sebserver.examconfig.status.READY_TO_USE=Ready To Use
@ -603,9 +705,9 @@ sebserver.examconfig.props.form.views.hooked_keys=Hooked Keys
sebserver.examconfig.props.label.hashedAdminPassword=Administrator password
sebserver.examconfig.props.label.hashedAdminPassword.confirm=Confirm password
sebserver.examconfig.props.label.allowQuit=Allow user to quit SEB
sebserver.examconfig.props.label.allowQuit.tooltip=Users can quit SEB with Control-Q, window close or quit button.</br>Otherwise use a quit link in your exam system or shutdown/restart the computer.
sebserver.examconfig.props.label.allowQuit.tooltip=Users can quit SEB with Control-Q, window close or quit button.<br/>Otherwise use a quit link in your exam system or shutdown/restart the computer.
sebserver.examconfig.props.label.ignoreExitKeys=Ignore exit keys
sebserver.examconfig.props.label.ignoreExitKeys.tooltip=SEB ignores the exit keys and can only be quit manually by entering the quit password.</br>(click Quit button in SEB taskbar, press Ctrl-Q or click the main browser window close button)
sebserver.examconfig.props.label.ignoreExitKeys.tooltip=SEB ignores the exit keys and can only be quit manually by entering the quit password.<br/>(click Quit button in SEB taskbar, press Ctrl-Q or click the main browser window close button)
sebserver.examconfig.props.label.hashedQuitPassword=Quit/unlock password
sebserver.examconfig.props.label.hashedQuitPassword.confirm=Confirm password
sebserver.examconfig.props.group.exitSequence=Exit Sequence
@ -645,15 +747,15 @@ sebserver.examconfig.props.label.mainBrowserWindowPositioning.2=Right
sebserver.examconfig.props.group.wintoolbar=Browser Window Toolbar
sebserver.examconfig.props.label.enableBrowserWindowToolbar=Enable browser window toolbar
sebserver.examconfig.props.label.enableBrowserWindowToolbar.tooltip=Displays a toolbar on top of the browser window</br>which can also be hidden by the user.
sebserver.examconfig.props.label.enableBrowserWindowToolbar.tooltip=Displays a toolbar on top of the browser window<br/>which can also be hidden by the user.
sebserver.examconfig.props.label.hideBrowserWindowToolbar=Hide toolbar as default (Mac)
sebserver.examconfig.props.label.hideBrowserWindowToolbar.tooltip=Hide browser window toolbar by default.</br>It can be shown again by using the View menu or Alt-Command-T.
sebserver.examconfig.props.label.hideBrowserWindowToolbar.tooltip=Hide browser window toolbar by default.<br/>It can be shown again by using the View menu or Alt-Command-T.
sebserver.examconfig.props.label.showMenuBar=Show menu bar (Mac)
sebserver.examconfig.props.label.showMenuBar.tooltip=Show the OS X menu bar to allow to access settings like Wi-Fi.
sebserver.examconfig.props.group.taskbar=SEB Taskbar/Dock
sebserver.examconfig.props.label.showTaskBar=Show SEB taskbar
sebserver.examconfig.props.label.showTaskBar.tooltip=The SEB task bar shows and switches between open browser windows,</br> allowed resources and applications and displays additional controls
sebserver.examconfig.props.label.showTaskBar.tooltip=The SEB task bar shows and switches between open browser windows,<br/> allowed resources and applications and displays additional controls
sebserver.examconfig.props.label.taskBarHeight=Taskbar/dock height
sebserver.examconfig.props.label.taskBarHeight.tooltip=Height of SEB dock/task bar in points/pixels
sebserver.examconfig.props.label.showReloadButton=Show reload button
@ -665,9 +767,9 @@ sebserver.examconfig.props.label.showInputLanguage.tooltip=Shows current keyboar
sebserver.examconfig.props.group.zoom=Enable Zoom (Win/Mac)
sebserver.examconfig.props.label.enableZoomPage=Enable page zoom
sebserver.examconfig.props.label.enableZoomPage.tooltip=Pages can be zoomed with ctrl - cmd +/-</br> or the commands in the view menu and browser window toolbar (Mac)
sebserver.examconfig.props.label.enableZoomPage.tooltip=Pages can be zoomed with ctrl - cmd +/-<br/> or the commands in the view menu and browser window toolbar (Mac)
sebserver.examconfig.props.label.enableZoomText=Enable text zoom
sebserver.examconfig.props.label.enableZoomText.tooltip=Text in browser windows can be zoomed with cmd +/-</br> or the commands in the view menu and browser window toolbar (Mac)
sebserver.examconfig.props.label.enableZoomText.tooltip=Text in browser windows can be zoomed with cmd +/-<br/> or the commands in the view menu and browser window toolbar (Mac)
sebserver.examconfig.props.group.zoomMode=Zoom Mode Win (Ctrl-Mousewheel)
sebserver.examconfig.props.label.zoomMode.0=Use page zoom
sebserver.examconfig.props.label.zoomMode.0.tooltip=Zoom whole web pages using Ctrl-Mousewheel (Win)"
@ -687,7 +789,7 @@ sebserver.examconfig.props.label.allowSpellCheck=Allow spell checking
sebserver.examconfig.props.label.allowSpellCheck.tooltip=Allow to use "Check spelling" in the SEB browser
sebserver.examconfig.props.label.allowDictionaryLookup=Allow dictionary lookup (Mac)
sebserver.examconfig.props.label.allowDictionaryLookup.tooltip=Allow to use the OS X dictionary lookup using a 3 finger tap
sebserver.examconfig.props.label.allowSpellCheckDictionary=The list below shows all dictionaries currently available for spell checking.</br>SEB comes with a list of standard dictionaries that can be activated/deactivated here.
sebserver.examconfig.props.label.allowSpellCheckDictionary=The list below shows all dictionaries currently available for spell checking.<br/>SEB comes with a list of standard dictionaries that can be activated/deactivated here.
sebserver.examconfig.props.label.allowSpellCheckDictionary.da-DK=Danish (Denmark) (da-DK)
sebserver.examconfig.props.label.allowSpellCheckDictionary.en-AU=English (Australia) (en-AU)
sebserver.examconfig.props.label.allowSpellCheckDictionary.en-GB=English (United Kingdom) (en-GB)
@ -702,8 +804,8 @@ sebserver.examconfig.props.group.newBrowserWindow=Links requesting to be opened
sebserver.examconfig.props.label.newBrowserWindowByLinkPolicy.0=get generally blocked
sebserver.examconfig.props.label.newBrowserWindowByLinkPolicy.1=open in same window
sebserver.examconfig.props.label.newBrowserWindowByLinkPolicy.2=open in new window
sebserver.examconfig.props.label.newBrowserWindowByLinkBlockForeign=Block when directing</br>to a different server
sebserver.examconfig.props.label.newBrowserWindowByLinkBlockForeign.tooltip=USE WITH CARE: Hyperlinks invoked by JavaScript/plug-ins</br> which direct to a different host than the one of the current main page will be ignored.
sebserver.examconfig.props.label.newBrowserWindowByLinkBlockForeign=Block when directing<br/>to a different server
sebserver.examconfig.props.label.newBrowserWindowByLinkBlockForeign.tooltip=USE WITH CARE: Hyperlinks invoked by JavaScript/plug-ins<br/> which direct to a different host than the one of the current main page will be ignored.
sebserver.examconfig.props.group.newwinsize=New browser window size and position
sebserver.examconfig.props.label.newBrowserWindowByLinkWidth=Width
@ -717,19 +819,19 @@ sebserver.examconfig.props.label.newBrowserWindowByLinkPositioning.2=Right
sebserver.examconfig.props.group.browserSecurity=Browser security
sebserver.examconfig.props.label.enablePlugIns=Enable plug-ins (Win: only Flash)
sebserver.examconfig.props.label.enablePlugIns.tooltip=Enables web plugins (Mac) or just Flash (Win).</br> For security reasons it\'s recommended to disable this option if you don\'t use any plugin/Flash content.
sebserver.examconfig.props.label.enablePlugIns.tooltip=Enables web plugins (Mac) or just Flash (Win).<br/> For security reasons it\'s recommended to disable this option if you don\'t use any plugin/Flash content.
sebserver.examconfig.props.label.enableJavaScript=Enable JavaScript
sebserver.examconfig.props.label.enableJavaScript.tooltip=Enables JavaScript.</br> Please note that most modern web-sites need JavaScript for full functionality.
sebserver.examconfig.props.label.enableJavaScript.tooltip=Enables JavaScript.<br/> Please note that most modern web-sites need JavaScript for full functionality.
sebserver.examconfig.props.label.enableJava=Enable Java
sebserver.examconfig.props.label.enableJava.tooltip=Enables Java applets.</br> Note: Only applets with the highest Java security level will run in SEB.
sebserver.examconfig.props.label.enableJava.tooltip=Enables Java applets.<br/> Note: Only applets with the highest Java security level will run in SEB.
sebserver.examconfig.props.label.blockPopUpWindows=Block pop-up windows
sebserver.examconfig.props.label.blockPopUpWindows.tooltip=Disables pop-up windows</br> (often advertisement) opened by JavaScript without an user action such as a button click.
sebserver.examconfig.props.label.blockPopUpWindows.tooltip=Disables pop-up windows<br/> (often advertisement) opened by JavaScript without an user action such as a button click.
sebserver.examconfig.props.label.allowVideoCapture=Allow video capture (webcam)
sebserver.examconfig.props.label.allowVideoCapture.tooltip=Allow web applications to access camera
sebserver.examconfig.props.label.allowAudioCapture=Allow audio capture (microphone)
sebserver.examconfig.props.label.allowAudioCapture.tooltip=Allow web applications to access microphone
sebserver.examconfig.props.label.allowBrowsingBackForward=Allow navigating back/forward in exam
sebserver.examconfig.props.label.allowBrowsingBackForward.tooltip=Disabling browsing to previously visited pages may increase security,</br> because browsing back might allow to leave an exam
sebserver.examconfig.props.label.allowBrowsingBackForward.tooltip=Disabling browsing to previously visited pages may increase security,<br/> because browsing back might allow to leave an exam
sebserver.examconfig.props.label.newBrowserWindowNavigation=Allow navigating in additional windows
sebserver.examconfig.props.label.browserWindowAllowReload=Allow reload exam
sebserver.examconfig.props.label.browserWindowAllowReload.tooltip=Allow reload in the exam window with F5 reload button (if displayed)
@ -742,7 +844,7 @@ sebserver.examconfig.props.label.newBrowserWindowShowReloadWarning.tooltip=User
sebserver.examconfig.props.label.removeBrowserProfile=Remove profile (Win)
sebserver.examconfig.props.label.removeBrowserProfile.tooltip=Remove XULRunner browser profile (containing caches and also local storage) when quitting SEB
sebserver.examconfig.props.label.removeLocalStorage=Disable local storage (Mac)
sebserver.examconfig.props.label.removeLocalStorage.tooltip=If your web application uses local storage, you have to be sure data is saved encrypted</br> and removed when no longer needed as SEB doesn't remove local storage
sebserver.examconfig.props.label.removeLocalStorage.tooltip=If your web application uses local storage, you have to be sure data is saved encrypted<br/> and removed when no longer needed as SEB doesn't remove local storage
sebserver.examconfig.props.label.browserUserAgent=Suffix to be added to any user agent
sebserver.examconfig.props.group.userAgentDesktop=User agent for desktop mode
@ -750,7 +852,7 @@ sebserver.examconfig.props.label.browserUserAgentWinDesktopMode.0=Desktop defaul
sebserver.examconfig.props.label.browserUserAgentWinDesktopMode.0.tooltip=Zoom whole web pages using Ctrl-Mousewheel (Win)
sebserver.examconfig.props.label.browserUserAgentWinDesktopMode.1=Custom
sebserver.examconfig.props.label.browserUserAgentWinDesktopMode.1.tooltip=Zoom only text on web pages using Ctrl-Mousewheel (Win)
sebserver.examconfig.props.label.browserUserAgentWinDesktopModeCustom.tooltip=Custom desktop user agent string</br>(SEB appends its version number automatically)
sebserver.examconfig.props.label.browserUserAgentWinDesktopModeCustom.tooltip=Custom desktop user agent string<br/>(SEB appends its version number automatically)
sebserver.examconfig.props.group.userAgentTouch=User agent for touch/table mode
sebserver.examconfig.props.label.browserUserAgentWinTouchMode.0=Touch default
@ -764,28 +866,28 @@ sebserver.examconfig.props.label.browserUserAgentMac.1=Custom
sebserver.examconfig.props.label.browserUserAgentMac.1.tooltip=Zoom only text on web pages using Ctrl-Mousewheel (Win)
sebserver.examconfig.props.label.enableSebBrowser=Enable SEB with browser window
sebserver.examconfig.props.label.enableSebBrowser.tooltip=Disable this to start another application in kiosk mode</br>(for example a virtual desktop infrastructure client)
sebserver.examconfig.props.label.enableSebBrowser.tooltip=Disable this to start another application in kiosk mode<br/>(for example a virtual desktop infrastructure client)
sebserver.examconfig.props.label.browserWindowTitleSuffix=Suffix to be added to every browser window
sebserver.examconfig.props.label.allowDownUploads=Allow downloading and uploading files (Mac)
sebserver.examconfig.props.label.allowDownUpload.tooltip=Usually to be used with permitted third party applications</br> for which you want to provide files to be down-loaded.
sebserver.examconfig.props.label.allowDownUpload.tooltip=Usually to be used with permitted third party applications<br/> for which you want to provide files to be down-loaded.
sebserver.examconfig.props.label.downloadDirectoryWin=Download directory (Win)
sebserver.examconfig.props.label.downloadDirectoryOSX=Download directory (Mac)
sebserver.examconfig.props.label.openDownloads=Open files after downloading (Mac)
sebserver.examconfig.props.label.chooseFileToUploadPolicy=Choose file to upload (Mac)
sebserver.examconfig.props.label.chooseFileToUploadPolicy.tooltip=SEB can let users choose the file to upload or automatically use the same file which was down-loaded before.</br>If not found, a file requester or an error is presented depending on this setting.
sebserver.examconfig.props.label.chooseFileToUploadPolicy.tooltip=SEB can let users choose the file to upload or automatically use the same file which was down-loaded before.<br/>If not found, a file requester or an error is presented depending on this setting.
sebserver.examconfig.props.label.chooseFileToUploadPolicy.0=manually with file requester
sebserver.examconfig.props.label.chooseFileToUploadPolicy.1=by attempting to upload the same file downloaded before
sebserver.examconfig.props.label.chooseFileToUploadPolicy.2=by only allowing to upload the same file downloaded before
sebserver.examconfig.props.label.downloadPDFFiles=Download and open PDF files instead of displaying them inline (Mac)
sebserver.examconfig.props.label.downloadPDFFiles.tooltip=PDF files will not be displayed by SEB but downloaded and openend (if "Open files after downloading" is active!)</br> by the application set in Finder (usually Preview or Adobe Acrobat).
sebserver.examconfig.props.label.downloadPDFFiles.tooltip=PDF files will not be displayed by SEB but downloaded and openend (if "Open files after downloading" is active!)<br/> by the application set in Finder (usually Preview or Adobe Acrobat).
sebserver.examconfig.props.label.allowPDFPlugIn=Allow using Acrobat Reader PDF plugin (insecure! Mac only)
sebserver.examconfig.props.label.allowPDFPlugIn.tooltip=The Adobe Acrobat Reader browser plugin should only be used on secured managed Mac computers,</br> at it allows limited access the file system and unlimited to cloud services
sebserver.examconfig.props.label.allowPDFPlugIn.tooltip=The Adobe Acrobat Reader browser plugin should only be used on secured managed Mac computers,<br/> at it allows limited access the file system and unlimited to cloud services
sebserver.examconfig.props.label.downloadAndOpenSebConfig=Download and open SEB Config Files
sebserver.examconfig.props.label.downloadAndOpenSebConfig.tooltip=Download and open .seb config files regardless if downloading and opening other file types is allowed.
sebserver.examconfig.props.group.quitLink=Link to quit SEB after exam
sebserver.examconfig.props.label.quitURL=Place this quit link to the 'feedback' page displayed after an exam was successfully finished.</br> Clicking that link will quit SEB without having to enter the quit password.
sebserver.examconfig.props.label.quitURL=Place this quit link to the 'feedback' page displayed after an exam was successfully finished.<br/> Clicking that link will quit SEB without having to enter the quit password.
sebserver.examconfig.props.label.quitURLConfirm=Ask user to confirm quitting
sebserver.examconfig.props.group.backToStart=Back to Start Button
@ -798,7 +900,7 @@ sebserver.examconfig.props.label.restartExamPasswordProtected=Protect back to st
sebserver.examconfig.props.label.restartExamPasswordProtected.tooltip=The quit/restart password (if set) must be entered when the back to start button was pressed.
sebserver.examconfig.props.label.allowSwitchToApplications=Allow switching to third party application (Mac)
sebserver.examconfig.props.label.allowSwitchToApplications.tooltip=Decreases security of the kiosk mode by allowing process switcher (Cmd+Tab).</br> The blacked out background of SEB also doesn't cover some alerts and modal windows in this mode.
sebserver.examconfig.props.label.allowSwitchToApplications.tooltip=Decreases security of the kiosk mode by allowing process switcher (Cmd+Tab).<br/> The blacked out background of SEB also doesn't cover some alerts and modal windows in this mode.
sebserver.examconfig.props.label.allowFlashFullscreen=Allow Flash to switch to fullscreen mode (Mac)
sebserver.examconfig.props.label.permittedProcesses.add.tooltip=Add permitted process
sebserver.examconfig.props.label.permittedProcesses.remove.tooltip=Remove selected permitted process
@ -811,11 +913,11 @@ sebserver.examconfig.props.label.permittedProcesses.os.tooltip=Indicates on whic
sebserver.examconfig.props.label.permittedProcesses.os.0=OS X
sebserver.examconfig.props.label.permittedProcesses.os.1=Win
sebserver.examconfig.props.label.permittedProcesses.title=Title
sebserver.examconfig.props.label.permittedProcesses.title.tooltip=Application title which is displayed in the application chooser.</br> Background processes don't have a title, because they can't be selected by users.
sebserver.examconfig.props.label.permittedProcesses.title.tooltip=Application title which is displayed in the application chooser.<br/> Background processes don't have a title, because they can't be selected by users.
sebserver.examconfig.props.label.permittedProcesses.description=Description
sebserver.examconfig.props.label.permittedProcesses.description.tooltip=Optional, should explain what kind of process this is,</br> because this might not be obvious only from the executable's name.
sebserver.examconfig.props.label.permittedProcesses.description.tooltip=Optional, should explain what kind of process this is,<br/> because this might not be obvious only from the executable's name.
sebserver.examconfig.props.label.permittedProcesses.executable=Executable
sebserver.examconfig.props.label.permittedProcesses.executable.tooltip=File name of the executable, which should not contain any parts of a file system path,</br> only the filename of the exe file (like calc.exe).
sebserver.examconfig.props.label.permittedProcesses.executable.tooltip=File name of the executable, which should not contain any parts of a file system path,<br/> only the filename of the exe file (like calc.exe).
sebserver.examconfig.props.label.permittedProcesses.originalName=Original Name
sebserver.examconfig.props.label.permittedProcesses.allowedExecutables=Window handling process
sebserver.examconfig.props.label.permittedProcesses.path=Path
@ -825,13 +927,13 @@ sebserver.examconfig.props.label.permittedProcesses.arguments.argument=Argument
sebserver.examconfig.props.label.permittedProcesses.arguments.addAction=Add new argument
sebserver.examconfig.props.label.permittedProcesses.arguments.removeAction=Remove this argument
sebserver.examconfig.props.label.permittedProcesses.identifier=Identifier
sebserver.examconfig.props.label.permittedProcesses.identifier.tooltip=(Sub) string in the title of the main window of a tricky third party application (Java, Acrobat etc.).</br> Mac OS X: Bundle identifier of the process in reverse domain notation.
sebserver.examconfig.props.label.permittedProcesses.identifier.tooltip=(Sub) string in the title of the main window of a tricky third party application (Java, Acrobat etc.).<br/> Mac OS X: Bundle identifier of the process in reverse domain notation.
sebserver.examconfig.props.label.permittedProcesses.iconInTaskbar=Icon in taskbar
sebserver.examconfig.props.label.permittedProcesses.iconInTaskbar.tooltip=Show icon of permitted application in task bar</br> (not possible when 'run in background' is enabled).
sebserver.examconfig.props.label.permittedProcesses.iconInTaskbar.tooltip=Show icon of permitted application in task bar<br/> (not possible when 'run in background' is enabled).
sebserver.examconfig.props.label.permittedProcesses.autostart=Autostart
sebserver.examconfig.props.label.permittedProcesses.autostart.tooltip=Start the process automatically together with SEB.
sebserver.examconfig.props.label.permittedProcesses.runInBackground=Allow running in background
sebserver.examconfig.props.label.permittedProcesses.runInBackground.tooltip=Allow the permitted process to already be running when SEB starts.</br> Such a process can't have an icon in the task bar.
sebserver.examconfig.props.label.permittedProcesses.runInBackground.tooltip=Allow the permitted process to already be running when SEB starts.<br/> Such a process can't have an icon in the task bar.
sebserver.examconfig.props.label.permittedProcesses.allowUserToChooseApp=Allow user to select location of application
sebserver.examconfig.props.label.permittedProcesses.strongKill=Force quit (risk of data loss)
sebserver.examconfig.props.label.permittedProcesses.strongKill.tooltip=Terminate process in a not-nice way, which may cause data loss if the application had unsaved data
@ -846,15 +948,15 @@ sebserver.examconfig.props.label.prohibitedProcesses.os=OS
sebserver.examconfig.props.label.prohibitedProcesses.os.0=OS X
sebserver.examconfig.props.label.prohibitedProcesses.os.1=Win
sebserver.examconfig.props.label.prohibitedProcesses.description=Description
sebserver.examconfig.props.label.prohibitedProcesses.description.tooltip=Optional, to explain what kind of process this is,</br> because this might not be obvious only from the executable's name.
sebserver.examconfig.props.label.prohibitedProcesses.description.tooltip=Optional, to explain what kind of process this is,<br/> because this might not be obvious only from the executable's name.
sebserver.examconfig.props.label.prohibitedProcesses.executable=Executable
sebserver.examconfig.props.label.prohibitedProcesses.executable.tooltip=File name of the executable, which should not contain any parts of a file system path,</br> only the filename of the exe file (like calc.exe).
sebserver.examconfig.props.label.prohibitedProcesses.executable.tooltip=File name of the executable, which should not contain any parts of a file system path,<br/> only the filename of the exe file (like calc.exe).
sebserver.examconfig.props.label.prohibitedProcesses.originalName=Original Name
sebserver.examconfig.props.label.prohibitedProcesses.originalName.tooltip=Original file name (optional)
sebserver.examconfig.props.label.prohibitedProcesses.identifier=Identifier
sebserver.examconfig.props.label.prohibitedProcesses.identifier.tooltip=Title of the main window of a Java third party application.</br> Mac OS X: Bundle identifier of the process in reverse domain notation.
sebserver.examconfig.props.label.prohibitedProcesses.identifier.tooltip=Title of the main window of a Java third party application.<br/> Mac OS X: Bundle identifier of the process in reverse domain notation.
sebserver.examconfig.props.label.prohibitedProcesses.strongKill=Force quit (risk of data loss)
sebserver.examconfig.props.label.prohibitedProcesses.strongKill.tooltip=Terminate process in a not-nice way,</br> which may cause data loss if the application had unsaved data
sebserver.examconfig.props.label.prohibitedProcesses.strongKill.tooltip=Terminate process in a not-nice way,<br/> which may cause data loss if the application had unsaved data
sebserver.examconfig.props.group.urlFilter=Filter
sebserver.examconfig.props.label.URLFilterEnable=Activate URL Filtering
@ -1014,7 +1116,7 @@ sebserver.examconfig.props.label.insideSebEnableLogOff.tooltip=Activates the but
sebserver.examconfig.props.label.insideSebEnableShutDown=Enable Shut down
sebserver.examconfig.props.label.insideSebEnableShutDown.tooltip=Activates the button "Shutdown"
sebserver.examconfig.props.label.insideSebEnableEaseOfAccess=Enable Ease of Access
sebserver.examconfig.props.label.insideSebEnableEaseOfAccess.tooltip=Shows options when the button "Ease of Access" in the lower left corner is clicked,</br>which offers help e.g. to visually or aurally handicapped persons, like the Magnifier Glass.
sebserver.examconfig.props.label.insideSebEnableEaseOfAccess.tooltip=Shows options when the button "Ease of Access" in the lower left corner is clicked,<br/>which offers help e.g. to visually or aurally handicapped persons, like the Magnifier Glass.
sebserver.examconfig.props.label.insideSebEnableVmWareClientShade=Enable VMware Client Shade
sebserver.examconfig.props.label.insideSebEnableVmWareClientShade.tooltip=Activates the "Shade" bar at the upper edge of a virtual desktop, if existent. If you're not using VMware, this setting doesn't have any effect.
sebserver.examconfig.props.label.insideSebEnableNetworkConnectionSelector=Enable network connection selector
@ -1103,13 +1205,13 @@ sebserver.configtemplate.attr.type.COMPOSITE_TABLE=Table
sebserver.configtemplate.attrs.list.title=Configuration Attributes
sebserver.configtemplate.attrs.list.title.tooltip=Table of all SEB exam configuration attributes of this template
sebserver.configtemplate.attrs.list.name=Name
sebserver.configtemplate.attrs.list.name.tooltip=The technical name of the SEB exam configuration attribute with the display name in brackets if available.</br></br>{0}
sebserver.configtemplate.attrs.list.name.tooltip=The technical name of the SEB exam configuration attribute with the display name in brackets if available.<br/><br/>{0}
sebserver.configtemplate.attrs.list.view=View
sebserver.configtemplate.attrs.list.view.tooltip=The view/tab where the SEB exam configuration attribute belongs to.</br></br>{0}
sebserver.configtemplate.attrs.list.view.tooltip=The view/tab where the SEB exam configuration attribute belongs to.<br/><br/>{0}
sebserver.configtemplate.attrs.list.group=Group
sebserver.configtemplate.attrs.list.group.tooltip=The group on the view/tab where the SEB exam configuration attribute belongs to.</br></br>{0}
sebserver.configtemplate.attrs.list.group.tooltip=The group on the view/tab where the SEB exam configuration attribute belongs to.<br/><br/>{0}
sebserver.configtemplate.attrs.list.type=Type
sebserver.configtemplate.attrs.list.type.tooltip=The type of the SEB exam configuration attribute.</br></br>{0}
sebserver.configtemplate.attrs.list.type.tooltip=The type of the SEB exam configuration attribute.<br/><br/>{0}
sebserver.configtemplate.attr.list.actions=
sebserver.configtemplate.attr.list.actions.modify=Edit Attribute
@ -1149,17 +1251,31 @@ sebserver.monitoring.exam.info.pleaseSelect=Please select first an Exam from the
sebserver.monitoring.exam.list.empty=There are currently no running exams
sebserver.monitoring.exam.list.column.name=Name
sebserver.monitoring.exam.list.column.name.tooltip=The name of the exam.<br/><br/>Use the filter above to narrow down a specific exam name.<br/>{0}
sebserver.monitoring.exam.list.column.type=Type
sebserver.monitoring.exam.list.column.type.tooltip=The type of the exam.<br/><br/>Use the filter above to set a specific exam type.<br/>{0}
sebserver.monitoring.exam.list.column.startTime=Start Time {0}
sebserver.monitoring.exam.list.column.startTime.tooltip=The start date and time of the exam.<br/><br/>{0}
sebserver.monitoring.exam.list.column.endTime=End Time {0}
sebserver.monitoring.exam.list.column.endTime.tooltip=The end date and time of the exam.<br/><br/>{0}
sebserver.monitoring.exam=Monitoring Exam: {0}
sebserver.monitoring.connection.list.column.id=Client Identifier
sebserver.monitoring.connection.list.column.id=User Name or Session
sebserver.monitoring.connection.list.column.id.tooltip=The user session identifier or username sent by the SEB client after LMS login.
sebserver.monitoring.connection.list.column.address=IP Address
sebserver.monitoring.connection.list.column.address.tooltip=The IP address from the host the SEB client is connecting to the SEB Server.
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.connection.list.column.status.tooltip=The current connection status
sebserver.monitoring.connection.form.id=User Name or Session
sebserver.monitoring.connection.form.id.tooltip=The user session identifier or username sent by the SEB client after LMS login.
sebserver.monitoring.connection.form.address=IP Address
sebserver.monitoring.connection.form.address.tooltip=The IP address from the host the SEB client is connecting to the SEB Server.
sebserver.monitoring.connection.form.status=Status
sebserver.monitoring.connection.form.status.tooltip=The current connection status
sebserver.monitoring.connection.form.exam=Exam
sebserver.monitoring.connection.form.exam.tooltip=The exam name
sebserver.monitoring.exam.connection.emptySelection=Please select first a Connection from the list
sebserver.monitoring.exam.connection.emptySelection.active=Please select first an active Connection from the list
@ -1186,10 +1302,15 @@ sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined
sebserver.monitoring.exam.connection.eventlist.title=Events
sebserver.monitoring.exam.connection.eventlist.empty=No event found
sebserver.monitoring.exam.connection.eventlist.type=Event Type
sebserver.monitoring.exam.connection.eventlist.type.tooltip=The type of the log event.<br/><br/>Use the filter above to set a specific event type.<br/>{0}
sebserver.monitoring.exam.connection.eventlist.clienttime=Client Time {0}
sebserver.monitoring.exam.connection.eventlist.clienttime.tooltip=The time the SEB client has sent within the log event.<br/><br/>{0}
sebserver.monitoring.exam.connection.eventlist.servertime=Server Time {0}
sebserver.monitoring.exam.connection.eventlist.servertime.tooltip=The exact time (UTC) the SEB Server has received the log event.<br/><br/>{0}
sebserver.monitoring.exam.connection.eventlist.value=Value
sebserver.monitoring.exam.connection.eventlist.value.tooltip=The value of the log event.<br/><br/>{0}
sebserver.monitoring.exam.connection.eventlist.text=Text
sebserver.monitoring.exam.connection.eventlist.text.tooltip=The text of the log event.<br/><br/>{0}
sebserver.monitoring.exam.connection.event.type.UNKNOWN=Unknown
sebserver.monitoring.exam.connection.event.type.DEBUG_LOG=Debug
@ -1218,15 +1339,15 @@ sebserver.logs.activity.seblogs.details=Show Details
sebserver.userlogs.list.title=User Activity Logs
sebserver.userlogs.list.column.institution=Institution
sebserver.userlogs.list.column.institution.tooltip=The institution of the user activity log.</br></br>Use the filter above to specify the institution.</br>{0}
sebserver.userlogs.list.column.institution.tooltip=The institution of the user activity log.<br/><br/>Use the filter above to specify the institution.<br/>{0}
sebserver.userlogs.list.column.user=User
sebserver.userlogs.list.column.user.tooltip=The user account of the user activity log.</br></br>Use the filter above to specify a user account.</br>{0}
sebserver.userlogs.list.column.user.tooltip=The user account of the user activity log.<br/><br/>Use the filter above to specify a user account.<br/>{0}
sebserver.userlogs.list.column.dateTime=Date {0}
sebserver.userlogs.list.column.dateTime.tooltip=The date when the user activity log happened.</br></br>Use the filter above to specify a from- and to-date range.</br>{0}
sebserver.userlogs.list.column.dateTime.tooltip=The date when the user activity log happened.<br/><br/>Use the filter above to specify a from- and to-date range.<br/>{0}
sebserver.userlogs.list.column.activityType=User Activity
sebserver.userlogs.list.column.activityType.tooltip=The type of the user activity.</br></br>Use the filter above to specify a activity type.</br>{0}
sebserver.userlogs.list.column.activityType.tooltip=The type of the user activity.<br/><br/>Use the filter above to specify a activity type.<br/>{0}
sebserver.userlogs.list.column.entityType=Domain Type
sebserver.userlogs.list.column.entityType.tooltip=The domain type of the user activity.</br></br>Use the filter above to specify a domain type.</br>{0}
sebserver.userlogs.list.column.entityType.tooltip=The domain type of the user activity.<br/><br/>Use the filter above to specify a domain type.<br/>{0}
sebserver.userlogs.list.column.entityId=Entity-ID
sebserver.userlogs.list.column.message=Message
@ -1240,13 +1361,19 @@ sebserver.seblogs.list.title=SEB Client Logs
sebserver.seblogs.list.actions=
sebserver.seblogs.list.empty=No SEB client logs has been found. Please adapt or clear the filter
sebserver.seblogs.info.pleaseSelect=Please select first a SEB Client Log from the list
sebserver.seblogs.info.pleaseSelect=Please select first a SEB client Log from the list
sebserver.seblogs.list.column.institution=Institution
sebserver.seblogs.list.column.institution.tooltip=The institution where the exam belongs to.<br/><br/>Use the filter above to specify the institution.<br/>{0}
sebserver.seblogs.list.column.exam=Exam
sebserver.seblogs.list.column.exam.tooltip=The exam of the SEB client logs.<br/><br/>Use the filter above to specify an exam.<br/>{0}
sebserver.seblogs.list.column.client-session=User Session-ID
sebserver.seblogs.list.column.client-session.tooltip=The user or user-session identifier.<br/><br/>Use the filter above narrow down a user identifier name.<br/>{0}
sebserver.seblogs.list.column.type=Event Type
sebserver.seblogs.list.column.type.tooltip=The SEB client log event type.<br/><br/>Use the filter above to specify log type.<br/>{0}
sebserver.seblogs.list.column.time=Event Time {0}
sebserver.seblogs.list.column.time.tooltip=The SEB client log time.<br/><br/>Use the filter above to specify from- and to-date and time.<br/>{0}
sebserver.seblogs.list.column.value=Value
sebserver.seblogs.list.column.value.tooltip=The SEB client log value.<br/><br/>{0}
sebserver.seblogs.details.title=SEB Client Log Details
sebserver.seblogs.details.event.title=Event
@ -1255,19 +1382,34 @@ sebserver.seblogs.details.exam.title=Exam Details
sebserver.seblogs.details.dateTime=Date
sebserver.seblogs.form.column.client-session=Session-ID
sebserver.seblogs.form.column.client-session.tooltip=The user or user-session identifier.
sebserver.seblogs.form.column.type=Event Type
sebserver.seblogs.form.column.type.tooltip=The SEB client log event type.
sebserver.seblogs.form.column.server-time=Server Time
sebserver.seblogs.form.column.server-time.tooltip=The exact time when the SEB Server got the event log sent by an SEB client.
sebserver.seblogs.form.column.client-time=SEB Client Time
sebserver.seblogs.form.column.client-time.tooltip=The time that was send within the log from SEB client.
sebserver.seblogs.form.column.value=Value
sebserver.seblogs.form.column.value.tooltip=The SEB client log event value
sebserver.seblogs.form.column.message=Message
sebserver.seblogs.form.column.message.tooltip=The SEB client log message
sebserver.seblogs.form.column.connection.session-id=User Session-ID
sebserver.seblogs.form.column.connection.address=SEB Client Address
sebserver.seblogs.form.column.connection.session-id.tooltip=The user or user-session identifier.
sebserver.seblogs.form.column.connection.address=SEB client Address
sebserver.seblogs.form.column.connection.address.tooltip=The IP address of the SEB client
sebserver.seblogs.form.column.connection.token=SEB Connection Token
sebserver.seblogs.form.column.connection.token.tooltip=The connection token that was generated by the SEB Server to identify the SEB client connection.
sebserver.seblogs.form.column.connection.status=Connection Status
sebserver.seblogs.form.column.connection.status.tooltip=The current SEB client connection status.
sebserver.seblogs.form.column.exam.name=Name
sebserver.seblogs.form.column.exam.name.tooltip=The name of the exam.
sebserver.seblogs.form.column.exam.description=Description
sebserver.seblogs.form.column.exam.description.tooltip=The description of the exam.
sebserver.seblogs.form.column.exam.type=Type
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.endTime=End 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