SEBSERV-457 implementation
This commit is contained in:
parent
8584ff5312
commit
0544d7a799
38 changed files with 725 additions and 299 deletions
|
@ -47,6 +47,7 @@ public final class API {
|
|||
public static final String PARAM_VIEW_ID = "viewId";
|
||||
public static final String PARAM_INSTRUCTION_TYPE = "instructionType";
|
||||
public static final String PARAM_INSTRUCTION_ATTRIBUTES = "instructionAttributes";
|
||||
public static final String PARAM_ADDITIONAL_ATTRIBUTES = "additionalAttributes";
|
||||
|
||||
public static final String DEFAULT_CONFIG_TEMPLATE_ID = String.valueOf(ConfigurationNode.DEFAULT_TEMPLATE_ID);
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gbl.api;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
@ -19,6 +21,8 @@ import com.fasterxml.jackson.datatype.joda.JodaModule;
|
|||
@Component
|
||||
public class JSONMapper extends ObjectMapper {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JSONMapper.class);
|
||||
|
||||
private static final long serialVersionUID = 2883304481547670626L;
|
||||
|
||||
public JSONMapper() {
|
||||
|
@ -33,4 +37,15 @@ public class JSONMapper extends ObjectMapper {
|
|||
super.setSerializationInclusion(Include.NON_NULL);
|
||||
}
|
||||
|
||||
public String writeValueAsStringOr(final Object entity, final String or) {
|
||||
if (entity == null) {
|
||||
return or;
|
||||
}
|
||||
try {
|
||||
return super.writeValueAsString(entity);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to serialize value: {}", entity, e);
|
||||
return or;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import java.util.Map;
|
|||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
|
@ -63,8 +64,6 @@ public final class Exam implements GrantEntity {
|
|||
public static final String FILTER_CACHED_QUIZZES = "cached-quizzes";
|
||||
public static final String FILTER_ATTR_HIDE_MISSING = "show-missing";
|
||||
|
||||
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "additionalAttributes";
|
||||
|
||||
/** This attribute name is used to store the number of quiz recover attempts done by exam update process */
|
||||
public static final String ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS = "QUIZ_RECOVER_ATTEMPTS";
|
||||
/** This attribute name is used on exams to store the flag for indicating the signature key check */
|
||||
|
@ -100,7 +99,6 @@ public final class Exam implements GrantEntity {
|
|||
public final Long institutionId;
|
||||
|
||||
@JsonProperty(EXAM.ATTR_LMS_SETUP_ID)
|
||||
@NotNull
|
||||
public final Long lmsSetupId;
|
||||
|
||||
@JsonProperty(EXAM.ATTR_EXTERNAL_ID)
|
||||
|
@ -150,7 +148,7 @@ public final class Exam implements GrantEntity {
|
|||
@JsonProperty(EXAM.ATTR_LAST_MODIFIED)
|
||||
public final Long lastModified;
|
||||
|
||||
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES)
|
||||
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES)
|
||||
public final Map<String, String> additionalAttributes;
|
||||
|
||||
@JsonIgnore
|
||||
|
@ -178,7 +176,7 @@ public final class Exam implements GrantEntity {
|
|||
@JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate,
|
||||
@JsonProperty(EXAM.ATTR_EXAM_TEMPLATE_ID) final Long examTemplateId,
|
||||
@JsonProperty(EXAM.ATTR_LAST_MODIFIED) final Long lastModified,
|
||||
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
|
||||
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
|
||||
|
||||
this.id = id;
|
||||
this.institutionId = institutionId;
|
||||
|
@ -209,6 +207,41 @@ public final class Exam implements GrantEntity {
|
|||
this.allowedSEBVersions = initAllowedSEBVersions();
|
||||
}
|
||||
|
||||
public Exam(final POSTMapper postMap) {
|
||||
this.id = null;
|
||||
this.institutionId = postMap.getLong(EXAM.ATTR_INSTITUTION_ID);
|
||||
this.lmsSetupId = postMap.getLong(EXAM.ATTR_LMS_SETUP_ID);
|
||||
this.externalId = postMap.getString(EXAM.ATTR_EXTERNAL_ID);
|
||||
this.lmsAvailable = true;
|
||||
this.name = postMap.getString(EXAM.ATTR_QUIZ_NAME);
|
||||
this.startTime = postMap.getDateTime(EXAM.ATTR_QUIZ_START_TIME);
|
||||
this.endTime = postMap.getDateTime(EXAM.ATTR_QUIZ_END_TIME);
|
||||
this.type = postMap.getEnum(EXAM.ATTR_TYPE, ExamType.class, ExamType.UNDEFINED);
|
||||
this.owner = postMap.getString(EXAM.ATTR_OWNER);
|
||||
this.status = postMap.getEnum(EXAM.ATTR_STATUS, ExamStatus.class, getStatusFromDate(this.startTime, this.endTime));
|
||||
this.sebRestriction = null;
|
||||
this.browserExamKeys = null;
|
||||
this.active = postMap.getBoolean(EXAM.ATTR_ACTIVE);
|
||||
this.supporter = postMap.getStringSet(EXAM.ATTR_SUPPORTER);
|
||||
this.lastUpdate = null;
|
||||
this.examTemplateId = postMap.getLong(EXAM.ATTR_EXAM_TEMPLATE_ID);
|
||||
this.lastModified = null;
|
||||
|
||||
final Map<String, String> additionalAttributes = new HashMap<>();
|
||||
if (postMap.contains(QuizData.QUIZ_ATTR_DESCRIPTION)) {
|
||||
additionalAttributes.put(QuizData.QUIZ_ATTR_DESCRIPTION, postMap.getString(QuizData.QUIZ_ATTR_DESCRIPTION));
|
||||
}
|
||||
additionalAttributes.put(QuizData.QUIZ_ATTR_START_URL, postMap.getString(QuizData.QUIZ_ATTR_START_URL));
|
||||
this.additionalAttributes = Utils.immutableMapOf(additionalAttributes);
|
||||
|
||||
this.checkASK = BooleanUtils
|
||||
.toBoolean(this.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
|
||||
this.allowedSEBVersions = initAllowedSEBVersions();
|
||||
}
|
||||
|
||||
public Exam(final QuizData quizData) {
|
||||
this(null, quizData, POSTMapper.EMPTY_MAP);
|
||||
}
|
||||
public Exam(final String modelId, final QuizData quizData, final POSTMapper mapper) {
|
||||
|
||||
final Map<String, String> additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes());
|
||||
|
@ -243,41 +276,13 @@ public final class Exam implements GrantEntity {
|
|||
this.allowedSEBVersions = initAllowedSEBVersions();
|
||||
}
|
||||
|
||||
public Exam(final QuizData quizData) {
|
||||
this(null, quizData, POSTMapper.EMPTY_MAP);
|
||||
}
|
||||
|
||||
public Exam(final Long id, final ExamStatus status) {
|
||||
this.id = id;
|
||||
this.institutionId = null;
|
||||
this.lmsSetupId = null;
|
||||
this.externalId = null;
|
||||
this.lmsAvailable = true;
|
||||
this.name = null;
|
||||
this.startTime = null;
|
||||
this.endTime = null;
|
||||
this.type = null;
|
||||
this.owner = null;
|
||||
this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime);
|
||||
this.sebRestriction = null;
|
||||
this.browserExamKeys = null;
|
||||
this.active = null;
|
||||
this.supporter = null;
|
||||
this.lastUpdate = null;
|
||||
this.examTemplateId = null;
|
||||
this.lastModified = null;
|
||||
this.additionalAttributes = null;
|
||||
this.checkASK = false;
|
||||
this.allowedSEBVersions = null;
|
||||
}
|
||||
|
||||
private List<AllowedSEBVersion> initAllowedSEBVersions() {
|
||||
if (this.additionalAttributes.containsKey(ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS)) {
|
||||
final String asvString = this.additionalAttributes.get(Exam.ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS);
|
||||
final String[] split = StringUtils.split(asvString, Constants.LIST_SEPARATOR);
|
||||
final List<AllowedSEBVersion> result = new ArrayList<>();
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
final AllowedSEBVersion allowedSEBVersion = new AllowedSEBVersion(split[i]);
|
||||
for (final String s : split) {
|
||||
final AllowedSEBVersion allowedSEBVersion = new AllowedSEBVersion(s);
|
||||
if (allowedSEBVersion.isValidFormat) {
|
||||
result.add(allowedSEBVersion);
|
||||
}
|
||||
|
@ -401,7 +406,7 @@ public final class Exam implements GrantEntity {
|
|||
}
|
||||
|
||||
public boolean additionalAttributesIncluded() {
|
||||
return this.additionalAttributes != null;
|
||||
return this.additionalAttributes != null && !this.additionalAttributes.isEmpty();
|
||||
}
|
||||
|
||||
public String getAdditionalAttribute(final String attrName) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import java.util.Comparator;
|
|||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
@ -33,9 +34,6 @@ public final class QuizData implements GrantEntity {
|
|||
|
||||
public static final String FILTER_ATTR_QUIZ_NAME = "quiz_name";
|
||||
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";
|
||||
|
@ -81,7 +79,7 @@ public final class QuizData implements GrantEntity {
|
|||
@JsonProperty(QUIZ_ATTR_START_URL)
|
||||
public final String startURL;
|
||||
|
||||
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES)
|
||||
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES)
|
||||
public final Map<String, String> additionalAttributes;
|
||||
|
||||
@JsonCreator
|
||||
|
@ -95,7 +93,7 @@ public final class QuizData implements GrantEntity {
|
|||
@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) {
|
||||
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
|
||||
|
||||
this.id = id;
|
||||
this.institutionId = institutionId;
|
||||
|
|
|
@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gbl.model.user;
|
|||
|
||||
import java.util.Map;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
|
@ -21,7 +22,6 @@ public class ExamineeAccountDetails {
|
|||
public static final String ATTR_NAME = "name";
|
||||
public static final String ATTR_USER_NAME = "username";
|
||||
public static final String ATTR_EMAIL = "email";
|
||||
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "additionalAttributes";
|
||||
|
||||
@JsonProperty(ATTR_ID)
|
||||
public final String id;
|
||||
|
@ -35,7 +35,7 @@ public class ExamineeAccountDetails {
|
|||
@JsonProperty(ATTR_EMAIL)
|
||||
public final String email;
|
||||
|
||||
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES)
|
||||
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES)
|
||||
public final Map<String, String> additionalAttributes;
|
||||
|
||||
@JsonCreator
|
||||
|
@ -44,7 +44,7 @@ public class ExamineeAccountDetails {
|
|||
@JsonProperty(ATTR_NAME) final String name,
|
||||
@JsonProperty(ATTR_USER_NAME) final String username,
|
||||
@JsonProperty(ATTR_EMAIL) final String email,
|
||||
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
|
||||
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
|
||||
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
|
|
|
@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gbl.util;
|
|||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
@ -244,6 +245,13 @@ public final class Result<T> {
|
|||
}
|
||||
}
|
||||
|
||||
public Result<T> whenDo(final Predicate<T> predicate, final Function<T, T> handler) {
|
||||
if (this.error == null && predicate.test(this.value)) {
|
||||
return Result.tryCatch(() -> handler.apply(this.value));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Result<T> onSuccess(final Consumer<T> handler) {
|
||||
if (this.error == null) {
|
||||
handler.accept(this.value);
|
||||
|
|
|
@ -278,13 +278,20 @@ public final class Utils {
|
|||
.getMillis());
|
||||
}
|
||||
|
||||
public static String formatDate(final DateTime dateTime) {
|
||||
public static String formatDateWithMilliseconds(final DateTime dateTime) {
|
||||
if (dateTime == null) {
|
||||
return Constants.EMPTY_NOTE;
|
||||
}
|
||||
return dateTime.toString(Constants.STANDARD_DATE_TIME_MILLIS_FORMATTER);
|
||||
}
|
||||
|
||||
public static String formatDate(final DateTime dateTime) {
|
||||
if (dateTime == null) {
|
||||
return Constants.EMPTY_NOTE;
|
||||
}
|
||||
return dateTime.toString(Constants.STANDARD_DATE_TIME_FORMATTER);
|
||||
}
|
||||
|
||||
public static Long dateTimeStringToTimestamp(final String startTime, final Long defaultValue) {
|
||||
return dateTimeStringToTimestamp(startTime)
|
||||
.getOr(defaultValue);
|
||||
|
|
|
@ -10,20 +10,17 @@ package ch.ethz.seb.sebserver.gui.content.exam;
|
|||
|
||||
import static ch.ethz.seb.sebserver.gbl.FeatureService.ConfigurableFeature.SCREEN_PROCTORING;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.FeatureService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.*;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.swt.layout.GridData;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
@ -65,13 +62,6 @@ import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
|||
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
|
||||
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.exam.ArchiveExam;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetDefaultExamTemplate;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplate;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup;
|
||||
|
@ -216,35 +206,33 @@ public class ExamForm implements TemplateComposer {
|
|||
@Override
|
||||
public void compose(final PageContext pageContext) {
|
||||
final CurrentUser currentUser = this.resourceService.getCurrentUser();
|
||||
|
||||
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
final boolean readonly = pageContext.isReadonly();
|
||||
final boolean newExamNoLMS = BooleanUtils.toBoolean(
|
||||
pageContext.getAttribute(AttributeKeys.NEW_EXAM_NO_LMS));
|
||||
final boolean importFromQuizData = BooleanUtils.toBoolean(
|
||||
pageContext.getAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA));
|
||||
|
||||
// get or create model data
|
||||
final Exam exam = (importFromQuizData
|
||||
? createExamFromQuizData(pageContext)
|
||||
: getExistingExam(pageContext))
|
||||
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
|
||||
.getOrThrow();
|
||||
final Exam exam = newExamNoLMS
|
||||
? this.newExamNoLMS()
|
||||
: (importFromQuizData
|
||||
? createExamFromQuizData(pageContext)
|
||||
: getExistingExam(pageContext))
|
||||
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
|
||||
.getOrThrow();
|
||||
|
||||
// new PageContext with actual EntityKey
|
||||
final EntityKey entityKey = (readonly || !newExamNoLMS) ? pageContext.getEntityKey() : null;
|
||||
final PageContext formContext = pageContext.withEntityKey(exam.getEntityKey());
|
||||
|
||||
final BooleanSupplier isNew = () -> importFromQuizData;
|
||||
final BooleanSupplier isNotNew = () -> !isNew.getAsBoolean();
|
||||
final EntityGrantCheck entityGrantCheck = currentUser.entityGrantCheck(exam);
|
||||
final boolean modifyGrant = entityGrantCheck.m();
|
||||
final boolean writeGrant = entityGrantCheck.w();
|
||||
final ExamStatus examStatus = exam.getStatus();
|
||||
final boolean editable = modifyGrant &&
|
||||
(examStatus == ExamStatus.UP_COMING || examStatus == ExamStatus.RUNNING);
|
||||
(exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.RUNNING);
|
||||
final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
|
||||
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
|
||||
|
||||
final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam);
|
||||
final boolean sebRestrictionAvailable = readonly && testSEBRestrictionAPI(exam);
|
||||
final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
|
||||
.getBuilder(CheckSEBRestriction.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId())
|
||||
|
@ -281,140 +269,15 @@ public class ExamForm implements TemplateComposer {
|
|||
}
|
||||
|
||||
// The Exam form
|
||||
final FormHandle<Exam> formHandle = this.pageService.formBuilder(
|
||||
formContext.copyOf(content), 8)
|
||||
.withDefaultSpanLabel(1)
|
||||
.withDefaultSpanInput(4)
|
||||
.withDefaultSpanEmptyCell(3)
|
||||
.readonly(readonly)
|
||||
.putStaticValueIf(isNotNew,
|
||||
Domain.EXAM.ATTR_ID,
|
||||
exam.getModelId())
|
||||
.putStaticValue(
|
||||
Domain.EXAM.ATTR_INSTITUTION_ID,
|
||||
String.valueOf(exam.getInstitutionId()))
|
||||
.putStaticValueIf(isNotNew,
|
||||
Domain.EXAM.ATTR_LMS_SETUP_ID,
|
||||
String.valueOf(exam.lmsSetupId))
|
||||
.putStaticValueIf(isNew,
|
||||
QuizData.QUIZ_ATTR_LMS_SETUP_ID,
|
||||
String.valueOf(exam.lmsSetupId))
|
||||
.putStaticValueIf(isNotNew,
|
||||
Domain.EXAM.ATTR_EXTERNAL_ID,
|
||||
exam.externalId)
|
||||
.putStaticValueIf(isNew,
|
||||
QuizData.QUIZ_ATTR_ID,
|
||||
exam.externalId)
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_NAME,
|
||||
FORM_NAME_TEXT_KEY,
|
||||
exam.name)
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.singleSelection(
|
||||
Domain.EXAM.ATTR_LMS_SETUP_ID,
|
||||
FORM_LMSSETUP_TEXT_KEY,
|
||||
String.valueOf(exam.lmsSetupId),
|
||||
this.resourceService::lmsSetupResource)
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_START_TIME,
|
||||
FORM_START_TIME_TEXT_KEY,
|
||||
i18nSupport.formatDisplayDateWithTimeZone(exam.startTime))
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_END_TIME,
|
||||
FORM_END_TIME_TEXT_KEY,
|
||||
i18nSupport.formatDisplayDateWithTimeZone(exam.endTime))
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_START_URL,
|
||||
FORM_QUIZ_URL_TEXT_KEY,
|
||||
exam.getStartURL())
|
||||
.readonly(true)
|
||||
.withInputSpan(7)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_DESCRIPTION,
|
||||
FORM_DESCRIPTION_TEXT_KEY,
|
||||
exam.getDescription())
|
||||
.asHTML(50)
|
||||
.readonly(true)
|
||||
.withInputSpan(6)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
Domain.EXAM.ATTR_EXTERNAL_ID,
|
||||
FORM_QUIZ_ID_TEXT_KEY,
|
||||
exam.externalId)
|
||||
.readonly(true)
|
||||
.withLabelSpan(2)
|
||||
.withInputSpan(6)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
Domain.EXAM.ATTR_STATUS + "_display",
|
||||
FORM_STATUS_TEXT_KEY,
|
||||
i18nSupport.getText(new LocTextKey("sebserver.exam.status." + examStatus.name())))
|
||||
.readonly(true)
|
||||
.withLabelSpan(2)
|
||||
.withInputSpan(4)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addFieldIf(
|
||||
() -> importFromQuizData,
|
||||
() -> FormBuilder.singleSelection(
|
||||
Domain.EXAM.ATTR_EXAM_TEMPLATE_ID,
|
||||
FORM_EXAM_TEMPLATE_TEXT_KEY,
|
||||
(exam.examTemplateId == null)
|
||||
? getDefaultExamTemplateId()
|
||||
: String.valueOf(exam.examTemplateId),
|
||||
this.resourceService::examTemplateResources)
|
||||
.withSelectionListener(form -> this.processTemplateSelection(form, formContext))
|
||||
.withLabelSpan(2)
|
||||
.withInputSpan(4)
|
||||
.withEmptyCellSpan(2))
|
||||
|
||||
.addField(FormBuilder.singleSelection(
|
||||
Domain.EXAM.ATTR_TYPE,
|
||||
FORM_TYPE_TEXT_KEY,
|
||||
(exam.type != null) ? String.valueOf(exam.type) : Exam.ExamType.UNDEFINED.name(),
|
||||
this.resourceService::examTypeResources)
|
||||
.withLabelSpan(2)
|
||||
.withInputSpan(4)
|
||||
.withEmptyCellSpan(2)
|
||||
.mandatory(!readonly))
|
||||
|
||||
.addField(FormBuilder.multiComboSelection(
|
||||
Domain.EXAM.ATTR_SUPPORTER,
|
||||
FORM_SUPPORTER_TEXT_KEY,
|
||||
StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR),
|
||||
this.resourceService::examSupporterResources)
|
||||
.withLabelSpan(2)
|
||||
.withInputSpan(4)
|
||||
.withEmptyCellSpan(2))
|
||||
|
||||
.buildFor(importFromQuizData
|
||||
? this.restService.getRestCall(ImportAsExam.class)
|
||||
: this.restService.getRestCall(SaveExam.class));
|
||||
final FormHandle<Exam> formHandle = readonly
|
||||
? createReadOnlyForm(formContext, content, exam)
|
||||
: createEditForm(formContext, content, exam);
|
||||
|
||||
if (importFromQuizData) {
|
||||
this.processTemplateSelection(formHandle.getForm(), formContext);
|
||||
}
|
||||
|
||||
final boolean proctoringEnabled = !importFromQuizData && this.restService
|
||||
final boolean proctoringEnabled = readonly && this.restService
|
||||
.getBuilder(GetExamProctoringSettings.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||
.call()
|
||||
|
@ -422,7 +285,7 @@ public class ExamForm implements TemplateComposer {
|
|||
.getOr(false);
|
||||
|
||||
final boolean spsFeatureEnabled = this.featureService.isEnabled(SCREEN_PROCTORING);
|
||||
final boolean screenProctoringEnabled = spsFeatureEnabled && !importFromQuizData && this.restService
|
||||
final boolean screenProctoringEnabled = readonly && spsFeatureEnabled && this.restService
|
||||
.getBuilder(GetScreenProctoringSettings.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||
.call()
|
||||
|
@ -450,7 +313,7 @@ public class ExamForm implements TemplateComposer {
|
|||
.withEntityKey(entityKey)
|
||||
.withConfirm(() -> EXAM_ARCHIVE_CONFIRM)
|
||||
.withExec(this::archiveExam)
|
||||
.publishIf(() -> writeGrant && readonly && examStatus == ExamStatus.FINISHED)
|
||||
.publishIf(() -> writeGrant && readonly && exam.status == ExamStatus.FINISHED)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SAVE)
|
||||
.withExec(action -> (importFromQuizData)
|
||||
|
@ -527,7 +390,6 @@ public class ExamForm implements TemplateComposer {
|
|||
.noEventPropagation()
|
||||
.publishIf(
|
||||
() -> spsFeatureEnabled && !screenProctoringEnabled && readonly)
|
||||
|
||||
;
|
||||
|
||||
// additional data in read-only view
|
||||
|
@ -538,7 +400,7 @@ public class ExamForm implements TemplateComposer {
|
|||
.copyOf(content)
|
||||
.withAttribute(ATTR_READ_GRANT, String.valueOf(entityGrantCheck.r()))
|
||||
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
|
||||
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
|
||||
.withAttribute(ATTR_EXAM_STATUS, exam.status.name()));
|
||||
|
||||
// Indicators
|
||||
this.examIndicatorsList.compose(
|
||||
|
@ -546,7 +408,7 @@ public class ExamForm implements TemplateComposer {
|
|||
.copyOf(content)
|
||||
.withAttribute(ATTR_READ_GRANT, String.valueOf(entityGrantCheck.r()))
|
||||
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
|
||||
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
|
||||
.withAttribute(ATTR_EXAM_STATUS, exam.status.name()));
|
||||
|
||||
// Client Groups
|
||||
this.examClientGroupList.compose(
|
||||
|
@ -554,10 +416,248 @@ public class ExamForm implements TemplateComposer {
|
|||
.copyOf(content)
|
||||
.withAttribute(ATTR_READ_GRANT, String.valueOf(entityGrantCheck.r()))
|
||||
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
|
||||
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
|
||||
.withAttribute(ATTR_EXAM_STATUS, exam.status.name()));
|
||||
}
|
||||
}
|
||||
|
||||
private FormHandle<Exam> createReadOnlyForm(
|
||||
final PageContext formContext,
|
||||
final Composite content,
|
||||
final Exam exam) {
|
||||
|
||||
final I18nSupport i18nSupport = formContext.getI18nSupport();
|
||||
return this.pageService.formBuilder(
|
||||
formContext.copyOf(content), 8)
|
||||
.withDefaultSpanLabel(1)
|
||||
.withDefaultSpanInput(4)
|
||||
.withDefaultSpanEmptyCell(3)
|
||||
.readonly(true)
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_NAME,
|
||||
FORM_NAME_TEXT_KEY,
|
||||
exam.name)
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.singleSelection(
|
||||
Domain.EXAM.ATTR_LMS_SETUP_ID,
|
||||
FORM_LMSSETUP_TEXT_KEY,
|
||||
String.valueOf(exam.lmsSetupId),
|
||||
this.resourceService::lmsSetupResource)
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
Domain.EXAM.ATTR_STATUS + "_display",
|
||||
FORM_STATUS_TEXT_KEY,
|
||||
i18nSupport.getText(new LocTextKey("sebserver.exam.status." + exam.status.name())))
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
Domain.EXAM.ATTR_EXTERNAL_ID,
|
||||
FORM_QUIZ_ID_TEXT_KEY,
|
||||
exam.externalId)
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_START_TIME,
|
||||
FORM_START_TIME_TEXT_KEY,
|
||||
i18nSupport.formatDisplayDateWithTimeZone(exam.startTime))
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_END_TIME,
|
||||
FORM_END_TIME_TEXT_KEY,
|
||||
i18nSupport.formatDisplayDateWithTimeZone(exam.endTime))
|
||||
.readonly(true)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_START_URL,
|
||||
FORM_QUIZ_URL_TEXT_KEY,
|
||||
exam.getStartURL())
|
||||
.readonly(true)
|
||||
.withInputSpan(7)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_DESCRIPTION,
|
||||
FORM_DESCRIPTION_TEXT_KEY,
|
||||
exam.getDescription())
|
||||
.asHTML(50)
|
||||
.readonly(true)
|
||||
.withInputSpan(7)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.singleSelection(
|
||||
Domain.EXAM.ATTR_TYPE,
|
||||
FORM_TYPE_TEXT_KEY,
|
||||
(exam.type != null) ? String.valueOf(exam.type) : Exam.ExamType.UNDEFINED.name(),
|
||||
this.resourceService::examTypeResources)
|
||||
.withInputSpan(7)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.multiComboSelection(
|
||||
Domain.EXAM.ATTR_SUPPORTER,
|
||||
FORM_SUPPORTER_TEXT_KEY,
|
||||
StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR),
|
||||
this.resourceService::examSupporterResources)
|
||||
.withInputSpan(7)
|
||||
.withEmptyCellSeparation(false))
|
||||
.build();
|
||||
}
|
||||
|
||||
private FormHandle<Exam> createEditForm(
|
||||
final PageContext formContext,
|
||||
final Composite content,
|
||||
final Exam exam) {
|
||||
|
||||
final I18nSupport i18nSupport = formContext.getI18nSupport();
|
||||
final boolean newExam = exam.id == null;
|
||||
final boolean hasLMS = exam.lmsSetupId != null;
|
||||
final boolean importFromLMS = newExam && hasLMS;
|
||||
final DateTimeZone timeZone = this.pageService.getCurrentUser().get().timeZone;
|
||||
final LocTextKey statusTitle = new LocTextKey("sebserver.exam.status." + exam.status.name());
|
||||
|
||||
return this.pageService.formBuilder(formContext.copyOf(content))
|
||||
.putStaticValueIf(() -> !newExam,
|
||||
Domain.EXAM.ATTR_ID,
|
||||
exam.getModelId())
|
||||
.putStaticValue(
|
||||
Domain.EXAM.ATTR_INSTITUTION_ID,
|
||||
String.valueOf(exam.getInstitutionId()))
|
||||
.putStaticValueIf(() -> exam.lmsSetupId != null,
|
||||
Domain.EXAM.ATTR_LMS_SETUP_ID,
|
||||
String.valueOf(exam.lmsSetupId))
|
||||
.putStaticValueIf(() -> exam.lmsSetupId != null,
|
||||
QuizData.QUIZ_ATTR_LMS_SETUP_ID,
|
||||
String.valueOf(exam.lmsSetupId))
|
||||
.putStaticValueIf(() -> exam.externalId != null,
|
||||
Domain.EXAM.ATTR_EXTERNAL_ID,
|
||||
exam.externalId)
|
||||
.putStaticValueIf(() -> exam.lmsSetupId != null,
|
||||
QuizData.QUIZ_ATTR_ID,
|
||||
exam.externalId)
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
Domain.EXAM.ATTR_STATUS + "_display",
|
||||
FORM_STATUS_TEXT_KEY,
|
||||
i18nSupport.getText(statusTitle))
|
||||
.readonly(true))
|
||||
|
||||
.addFieldIf( () -> hasLMS,
|
||||
() -> FormBuilder.singleSelection(
|
||||
Domain.EXAM.ATTR_LMS_SETUP_ID,
|
||||
FORM_LMSSETUP_TEXT_KEY,
|
||||
String.valueOf(exam.lmsSetupId),
|
||||
this.resourceService::lmsSetupResource)
|
||||
.readonly(true))
|
||||
|
||||
.addFieldIf(() -> exam.id == null,
|
||||
() -> FormBuilder.singleSelection(
|
||||
Domain.EXAM.ATTR_EXAM_TEMPLATE_ID,
|
||||
FORM_EXAM_TEMPLATE_TEXT_KEY,
|
||||
(exam.examTemplateId == null)
|
||||
? getDefaultExamTemplateId()
|
||||
: String.valueOf(exam.examTemplateId),
|
||||
this.resourceService::examTemplateResources)
|
||||
.withSelectionListener(form -> this.processTemplateSelection(form, formContext)))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
Domain.EXAM.ATTR_QUIZ_NAME,
|
||||
FORM_NAME_TEXT_KEY,
|
||||
exam.name)
|
||||
.readonly(hasLMS)
|
||||
.mandatory(!hasLMS))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_DESCRIPTION,
|
||||
FORM_DESCRIPTION_TEXT_KEY,
|
||||
exam.getDescription())
|
||||
.asArea()
|
||||
.readonly(hasLMS))
|
||||
.withAdditionalValueMapping(
|
||||
QuizData.QUIZ_ATTR_DESCRIPTION,
|
||||
QuizData.QUIZ_ATTR_DESCRIPTION)
|
||||
|
||||
.addField(FormBuilder.dateTime(
|
||||
Domain.EXAM.ATTR_QUIZ_START_TIME,
|
||||
FORM_START_TIME_TEXT_KEY,
|
||||
exam.startTime)
|
||||
.readonly(hasLMS)
|
||||
.mandatory(!hasLMS))
|
||||
|
||||
.addField(FormBuilder.dateTime(
|
||||
Domain.EXAM.ATTR_QUIZ_END_TIME,
|
||||
FORM_END_TIME_TEXT_KEY,
|
||||
exam.endTime != null
|
||||
? exam.endTime
|
||||
: DateTime.now(timeZone).plusHours(1))
|
||||
.readonly(hasLMS)
|
||||
.mandatory(!hasLMS))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
QuizData.QUIZ_ATTR_START_URL,
|
||||
FORM_QUIZ_URL_TEXT_KEY,
|
||||
exam.getStartURL())
|
||||
.readonly(hasLMS)
|
||||
.mandatory(!hasLMS))
|
||||
.withAdditionalValueMapping(
|
||||
QuizData.QUIZ_ATTR_START_URL,
|
||||
QuizData.QUIZ_ATTR_START_URL)
|
||||
|
||||
.addField(FormBuilder.singleSelection(
|
||||
Domain.EXAM.ATTR_TYPE,
|
||||
FORM_TYPE_TEXT_KEY,
|
||||
(exam.type != null) ? String.valueOf(exam.type) : Exam.ExamType.UNDEFINED.name(),
|
||||
this.resourceService::examTypeResources)
|
||||
.mandatory(true))
|
||||
|
||||
.addField(FormBuilder.multiComboSelection(
|
||||
Domain.EXAM.ATTR_SUPPORTER,
|
||||
FORM_SUPPORTER_TEXT_KEY,
|
||||
StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR),
|
||||
this.resourceService::examSupporterResources))
|
||||
|
||||
.buildFor(importFromLMS
|
||||
? this.restService.getRestCall(ImportAsExam.class)
|
||||
: newExam
|
||||
? this.restService.getRestCall(NewExam.class)
|
||||
: this.restService.getRestCall(SaveExam.class));
|
||||
}
|
||||
|
||||
private Exam newExamNoLMS() {
|
||||
return new Exam(
|
||||
null,
|
||||
this.pageService.getCurrentUser().get().institutionId,
|
||||
null,
|
||||
UUID.randomUUID().toString(),
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Exam.ExamType.UNDEFINED,
|
||||
null,
|
||||
null,
|
||||
ExamStatus.UP_COMING,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
private PageAction archiveExam(final PageAction action) {
|
||||
|
||||
this.restService.getBuilder(ArchiveExam.class)
|
||||
|
@ -660,7 +760,7 @@ public class ExamForm implements TemplateComposer {
|
|||
}
|
||||
|
||||
private boolean testSEBRestrictionAPI(final Exam exam) {
|
||||
if (!exam.isLmsAvailable() || exam.status == ExamStatus.ARCHIVED) {
|
||||
if (exam.lmsSetupId == null || !exam.isLmsAvailable() || exam.status == ExamStatus.ARCHIVED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,14 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.content.exam;
|
||||
|
||||
import static ch.ethz.seb.sebserver.gbl.FeatureService.ConfigurableFeature.EXAM_NO_LMS;
|
||||
import static ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys.NEW_EXAM_NO_LMS;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Function;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.FeatureService;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.eclipse.rap.rwt.RWT;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
|
@ -145,6 +149,7 @@ public class ExamList implements TemplateComposer {
|
|||
final CurrentUser currentUser = this.resourceService.getCurrentUser();
|
||||
final RestService restService = this.resourceService.getRestService();
|
||||
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
|
||||
final FeatureService featureService = this.pageService.getFeatureService();
|
||||
|
||||
// content page layout with title
|
||||
final Composite content = widgetFactory.defaultPageLayout(
|
||||
|
@ -251,7 +256,7 @@ public class ExamList implements TemplateComposer {
|
|||
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
|
||||
action -> modifyExam(action, table),
|
||||
EMPTY_SELECTION_TEXT_KEY)
|
||||
.publishIf(() -> userGrant.im(), false)
|
||||
.publishIf(userGrant::im, false)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_LIST_BULK_ARCHIVE)
|
||||
.withSelect(
|
||||
|
@ -259,7 +264,7 @@ public class ExamList implements TemplateComposer {
|
|||
this.examBatchArchivePopup.popupCreationFunction(pageContext),
|
||||
EMPTY_SELECTION_TEXT_KEY)
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> userGrant.im(), false)
|
||||
.publishIf(userGrant::im, false)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_LIST_BULK_DELETE)
|
||||
.withSelect(
|
||||
|
@ -267,9 +272,8 @@ public class ExamList implements TemplateComposer {
|
|||
this.examBatchDeletePopup.popupCreationFunction(pageContext),
|
||||
EMPTY_SELECTION_TEXT_KEY)
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> userGrant.iw(), false);
|
||||
.publishIf(userGrant::iw, false)
|
||||
|
||||
actionBuilder
|
||||
.newAction(ActionDefinition.EXAM_LIST_HIDE_MISSING)
|
||||
.withExec(action -> hideMissingExams(action, table))
|
||||
.noEventPropagation()
|
||||
|
@ -278,8 +282,12 @@ public class ExamList implements TemplateComposer {
|
|||
.withExec(action -> showMissingExams(action, table))
|
||||
.noEventPropagation()
|
||||
.create())
|
||||
.publish();
|
||||
.publish()
|
||||
|
||||
.newAction(ActionDefinition.EXAM_NEW)
|
||||
.withAttribute(NEW_EXAM_NO_LMS, Constants.TRUE_STRING)
|
||||
.publishIf(() -> userGrant.iw() && featureService.isEnabled(EXAM_NO_LMS))
|
||||
;
|
||||
}
|
||||
|
||||
private PageAction showMissingExams(final PageAction action, final EntityTable<Exam> table) {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package ch.ethz.seb.sebserver.gui.form;
|
||||
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
import ch.ethz.seb.sebserver.gui.widget.DateTimeSelector;
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.layout.GridData;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Control;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.eclipse.swt.widgets.Text;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
public class DateTimeSelectorFieldBuilder extends FieldBuilder<DateTime> {
|
||||
|
||||
public DateTimeSelectorFieldBuilder(final String name, final LocTextKey label, final DateTime value) {
|
||||
super(name, label, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
void build(final FormBuilder builder) {
|
||||
final boolean readonly = builder.readonly || this.readonly;
|
||||
final Control titleLabel = createTitleLabel(builder.formParent, builder, this);
|
||||
final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput);
|
||||
|
||||
if (readonly) {
|
||||
final Text label = new Text(fieldGrid, SWT.NONE);
|
||||
label.setText(builder.i18nSupport.formatDisplayDateTime(value) + " " + builder.i18nSupport.getUsersTimeZoneTitleSuffix());
|
||||
label.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, true));
|
||||
builder.form.putReadonlyField(this.name, titleLabel, label);
|
||||
return;
|
||||
}
|
||||
|
||||
final DateTimeSelector dateTimeSelector = new DateTimeSelector(
|
||||
fieldGrid,
|
||||
builder.widgetFactory,
|
||||
builder.pageService.getCurrentUser().get().timeZone,
|
||||
this.label.name,
|
||||
label.name);
|
||||
|
||||
dateTimeSelector.setValue(value);
|
||||
final Label errorLabel = createErrorLabel(fieldGrid);
|
||||
builder.form.putField(this.name, titleLabel, dateTimeSelector, errorLabel);
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ public abstract class FieldBuilder<T> {
|
|||
int spanLabel = -1;
|
||||
int spanInput = -1;
|
||||
int spanEmptyCell = -1;
|
||||
int titleValign = SWT.TOP;
|
||||
int titleValign = SWT.CENTER;
|
||||
Boolean autoEmptyCellSeparation = null;
|
||||
String group = null;
|
||||
boolean readonly = false;
|
||||
|
@ -39,7 +39,6 @@ public abstract class FieldBuilder<T> {
|
|||
String defaultLabel = null;
|
||||
boolean isMandatory = false;
|
||||
boolean rightLabel = false;
|
||||
|
||||
final String name;
|
||||
final LocTextKey label;
|
||||
final LocTextKey tooltip;
|
||||
|
@ -133,7 +132,7 @@ public abstract class FieldBuilder<T> {
|
|||
gridLayout.marginWidth = 0;
|
||||
gridLayout.marginRight = 0;
|
||||
infoGrid.setLayout(gridLayout);
|
||||
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
|
||||
final GridData gridData = new GridData(SWT.FILL, fieldBuilder.titleValign, true, true);
|
||||
gridData.horizontalSpan = (fieldBuilder.spanLabel > 0) ? fieldBuilder.spanLabel : 1;
|
||||
infoGrid.setLayoutData(gridData);
|
||||
|
||||
|
|
|
@ -8,25 +8,21 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.form;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gui.widget.*;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.rap.rwt.RWT;
|
||||
import org.eclipse.swt.browser.Browser;
|
||||
import org.eclipse.swt.graphics.Color;
|
||||
import org.eclipse.swt.widgets.Button;
|
||||
import org.eclipse.swt.widgets.Control;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.eclipse.swt.widgets.Text;
|
||||
import org.eclipse.swt.widgets.*;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
|
@ -40,12 +36,7 @@ import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
|||
import ch.ethz.seb.sebserver.gbl.util.Tuple;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.FormBinding;
|
||||
import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection;
|
||||
import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
|
||||
import ch.ethz.seb.sebserver.gui.widget.PasswordInput;
|
||||
import ch.ethz.seb.sebserver.gui.widget.Selection;
|
||||
import ch.ethz.seb.sebserver.gui.widget.Selection.Type;
|
||||
import ch.ethz.seb.sebserver.gui.widget.ThresholdList;
|
||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
|
||||
|
||||
public final class Form implements FormBinding {
|
||||
|
@ -55,9 +46,11 @@ public final class Form implements FormBinding {
|
|||
private final ObjectNode objectRoot;
|
||||
|
||||
private final Map<String, String> staticValues = new LinkedHashMap<>();
|
||||
private final Map<String, String> additionalAttributeMapping = new LinkedHashMap<>();
|
||||
private final MultiValueMap<String, FormFieldAccessor> formFields = new LinkedMultiValueMap<>();
|
||||
private final Map<String, Set<String>> groups = new LinkedHashMap<>();
|
||||
|
||||
|
||||
Form(final JSONMapper jsonMapper, final Cryptor cryptor) {
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.cryptor = cryptor;
|
||||
|
@ -108,6 +101,10 @@ public final class Form implements FormBinding {
|
|||
}
|
||||
}
|
||||
|
||||
public void putAdditionalValueMapping(final String fieldName, final String attrName) {
|
||||
this.additionalAttributeMapping.put(fieldName, attrName);
|
||||
}
|
||||
|
||||
public String getStaticValue(final String name) {
|
||||
return this.staticValues.get(name);
|
||||
}
|
||||
|
@ -175,14 +172,19 @@ public final class Form implements FormBinding {
|
|||
return this;
|
||||
}
|
||||
|
||||
Form putField(final String name, final Control label, final FileUploadSelection fileUpload,
|
||||
final Label errorLabel) {
|
||||
Form putField(final String name, final Control label, final FileUploadSelection fileUpload, final Label errorLabel) {
|
||||
final FormFieldAccessor createAccessor = createAccessor(label, fileUpload, errorLabel);
|
||||
fileUpload.setErrorHandler(createAccessor::setError);
|
||||
this.formFields.add(name, createAccessor);
|
||||
return this;
|
||||
}
|
||||
|
||||
Form putField(final String name, final Control label, final DateTimeSelector dateTimeSelector, final Label errorLabel) {
|
||||
final FormFieldAccessor createAccessor = createAccessor(label, dateTimeSelector, errorLabel);
|
||||
this.formFields.add(name, createAccessor);
|
||||
return this;
|
||||
}
|
||||
|
||||
Form removeField(final String name) {
|
||||
if (this.formFields.containsKey(name)) {
|
||||
final List<FormFieldAccessor> list = this.formFields.remove(name);
|
||||
|
@ -307,6 +309,21 @@ public final class Form implements FormBinding {
|
|||
.filter(Form::valueApplicationFilter)
|
||||
.forEach(ffa -> ffa.putJsonValue(entry.getKey(), this.objectRoot));
|
||||
}
|
||||
|
||||
if (!this.additionalAttributeMapping.isEmpty()) {
|
||||
final Map<String, String> additionalAttrs = new HashMap<>();
|
||||
for (final Map.Entry<String, String> entry : this.additionalAttributeMapping.entrySet()) {
|
||||
final String fieldValue = this.getFieldValue(entry.getKey());
|
||||
if (fieldValue != null) {
|
||||
additionalAttrs.put(entry.getValue(), fieldValue);
|
||||
}
|
||||
}
|
||||
if (additionalAttrs != null) {
|
||||
this.objectRoot.putIfAbsent(
|
||||
API.PARAM_ADDITIONAL_ATTRIBUTES,
|
||||
jsonMapper.valueToTree(additionalAttrs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean valueApplicationFilter(final FormFieldAccessor ffa) {
|
||||
|
@ -317,7 +334,7 @@ public final class Form implements FormBinding {
|
|||
//@formatter:off
|
||||
private FormFieldAccessor createReadonlyAccessor(final Control label, final Text field) {
|
||||
return new FormFieldAccessor(label, field, null) {
|
||||
@Override public String getStringValue() { return null; } // ensures that read-only fields do not send diplay values to the back-end
|
||||
@Override public String getStringValue() { return null; } // ensures that read-only fields do not send display values to the back-end
|
||||
@Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); }
|
||||
};
|
||||
}
|
||||
|
@ -412,6 +429,14 @@ public final class Form implements FormBinding {
|
|||
@Override public String getStringValue() { return fileUpload.getFileName(); }
|
||||
};
|
||||
}
|
||||
|
||||
private FormFieldAccessor createAccessor(final Control label, final DateTimeSelector dateTimeSelector, final Label errorLabel) {
|
||||
return new FormFieldAccessor(label, dateTimeSelector, errorLabel) {
|
||||
@Override public String getStringValue() { return dateTimeSelector.getValue(); }
|
||||
@Override public void setStringValue(final String value) { dateTimeSelector.setValue(value); }
|
||||
};
|
||||
}
|
||||
|
||||
//@formatter:on
|
||||
|
||||
/*
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.eclipse.swt.layout.GridData;
|
|||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.eclipse.swt.widgets.TabItem;
|
||||
import org.joda.time.DateTime;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -146,6 +147,11 @@ public class FormBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public FormBuilder withAdditionalValueMapping(final String fieldName, final String attrName) {
|
||||
this.form.putAdditionalValueMapping(fieldName, attrName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public FormBuilder addFieldIf(
|
||||
final BooleanSupplier condition,
|
||||
final Supplier<FieldBuilder<?>> templateSupplier) {
|
||||
|
@ -304,4 +310,8 @@ public class FormBuilder {
|
|||
(supportedFiles != null) ? Arrays.asList(supportedFiles) : Collections.emptyList());
|
||||
}
|
||||
|
||||
public static DateTimeSelectorFieldBuilder dateTime(final String name, final LocTextKey label, final DateTime dateTime) {
|
||||
return new DateTimeSelectorFieldBuilder(name, label, dateTime);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ public final class ImageUploadFieldBuilder extends FieldBuilder<String> {
|
|||
|
||||
ImageUploadFieldBuilder(final String name, final LocTextKey label, final String value) {
|
||||
super(name, label, value);
|
||||
super.titleValign = SWT.TOP;
|
||||
}
|
||||
|
||||
public ImageUploadFieldBuilder withMaxWidth(final int width) {
|
||||
|
|
|
@ -50,6 +50,7 @@ public final class SelectionFieldBuilder extends FieldBuilder<String> {
|
|||
super(name, label, value);
|
||||
this.type = type;
|
||||
this.itemsSupplier = itemsSupplier;
|
||||
super.titleValign = SWT.TOP;
|
||||
}
|
||||
|
||||
public SelectionFieldBuilder withSelectionListener(final Consumer<Form> selectionListener) {
|
||||
|
|
|
@ -63,28 +63,34 @@ public final class TextFieldBuilder extends FieldBuilder<String> {
|
|||
|
||||
public TextFieldBuilder asArea(final int minHeight) {
|
||||
this.areaMinHeight = minHeight;
|
||||
super.titleValign = SWT.TOP;
|
||||
return asArea();
|
||||
}
|
||||
|
||||
public TextFieldBuilder asArea() {
|
||||
this.isArea = true;
|
||||
this.titleValign = SWT.CENTER;
|
||||
this.titleValign = SWT.TOP;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TextFieldBuilder asHTML() {
|
||||
this.isHTML = true;
|
||||
super.titleValign = SWT.TOP;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TextFieldBuilder asHTML(final int minHeight) {
|
||||
this.isHTML = true;
|
||||
this.areaMinHeight = minHeight;
|
||||
super.titleValign = SWT.TOP;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FieldBuilder<?> asHTML(final boolean html) {
|
||||
this.isHTML = html;
|
||||
if (html) {
|
||||
super.titleValign = SWT.TOP;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ public interface I18nSupport {
|
|||
/** Format a DateTime to a text format to display.
|
||||
* This uses the date-format defined by either the attribute 'sebserver.gui.date.display format'
|
||||
* or the Constants.DEFAULT_DISPLAY_DATE_FORMAT
|
||||
*
|
||||
* <p>
|
||||
* Adds time-zone offset information if the currents user time-zone is different from UTC
|
||||
*
|
||||
* @param date the DateTime instance
|
||||
|
@ -55,7 +55,7 @@ public interface I18nSupport {
|
|||
/** Format a DateTime to a text format to display with additional time zone name at the end.
|
||||
* This uses the date-format defined by either the attribute 'sebserver.gui.date.display format'
|
||||
* or the Constants.DEFAULT_DISPLAY_DATE_FORMAT
|
||||
*
|
||||
* <p>
|
||||
* Adds time-zone offset information if the currents user time-zone is different from UTC
|
||||
*
|
||||
* @param date the DateTime instance
|
||||
|
@ -67,7 +67,7 @@ public interface I18nSupport {
|
|||
/** Format a time-stamp (milliseconds) to a text format to display.
|
||||
* This uses the date-format defined by either the attribute 'sebserver.gui.date.display format'
|
||||
* or the Constants.DEFAULT_DISPLAY_DATE_FORMAT
|
||||
*
|
||||
* <p>
|
||||
* Adds time-zone information if the currents user time-zone is different from UTC
|
||||
*
|
||||
* @param timestamp the unix-timestamp in milliseconds
|
||||
|
@ -79,7 +79,7 @@ public interface I18nSupport {
|
|||
/** Format a DateTime to a text format to display.
|
||||
* This uses the date-format defined by either the attribute 'sebserver.gui.datetime.display format'
|
||||
* or the Constants.DEFAULT_DISPLAY_DATE_TIME_FORMAT
|
||||
*
|
||||
* <p>
|
||||
* Adds time-zone information if the currents user time-zone is different from UTC
|
||||
*
|
||||
* @param date the DateTime instance
|
||||
|
@ -89,7 +89,7 @@ public interface I18nSupport {
|
|||
/** Format a time-stamp (milliseconds) to a text format to display.
|
||||
* This uses the date-format defined by either the attribute 'sebserver.gui.datetime.display format'
|
||||
* or the Constants.DEFAULT_DISPLAY_DATE_TIME_FORMAT
|
||||
*
|
||||
* <p>
|
||||
* Adds time-zone information if the currents user time-zone is different from UTC
|
||||
*
|
||||
* @param timestamp the unix-timestamp in milliseconds
|
||||
|
@ -101,7 +101,7 @@ public interface I18nSupport {
|
|||
/** Format a DateTime to a text format to display.
|
||||
* This uses the date-format defined by either the attribute 'sebserver.gui.time.display format'
|
||||
* or the Constants.DEFAULT_DISPLAY_TIME_FORMAT
|
||||
*
|
||||
* <p>
|
||||
* Adds time-zone information if the currents user time-zone is different from UTC
|
||||
*
|
||||
* @param date the DateTime instance
|
||||
|
@ -111,7 +111,7 @@ public interface I18nSupport {
|
|||
/** Format a time-stamp (milliseconds) to a text format to display.
|
||||
* This uses the date-format defined by either the attribute 'sebserver.gui.time.display format'
|
||||
* or the Constants.DEFAULT_DISPLAY_TIME_FORMAT
|
||||
*
|
||||
* <p>
|
||||
* Adds time-zone information if the currents user time-zone is different from UTC
|
||||
*
|
||||
* @param timestamp the unix-timestamp in milliseconds
|
||||
|
|
|
@ -44,6 +44,7 @@ public interface PageContext {
|
|||
String ENTITY_LIST_TYPE = "ENTITY_TYPE";
|
||||
|
||||
String IMPORT_FROM_QUIZ_DATA = "IMPORT_FROM_QUIZ_DATA";
|
||||
String NEW_EXAM_NO_LMS = "NEW_EXAM_NO_LMS";
|
||||
|
||||
String COPY_AS_TEMPLATE = "COPY_AS_TEMPLATE";
|
||||
String CREATE_FROM_TEMPLATE = "CREATE_FROM_TEMPLATE";
|
||||
|
|
|
@ -17,6 +17,7 @@ import java.util.function.Consumer;
|
|||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.FeatureService;
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.custom.ScrolledComposite;
|
||||
import org.eclipse.swt.graphics.Point;
|
||||
|
@ -72,6 +73,8 @@ public interface PageService {
|
|||
|
||||
Logger log = LoggerFactory.getLogger(PageService.class);
|
||||
|
||||
FeatureService getFeatureService();
|
||||
|
||||
/** Get the WidgetFactory service
|
||||
*
|
||||
* @return the WidgetFactory service */
|
||||
|
|
|
@ -220,13 +220,20 @@ public final class PageAction {
|
|||
}
|
||||
return Result.ofError(restCallError);
|
||||
} catch (final FormPostException e) {
|
||||
if (e.getCause() instanceof RestCallError) {
|
||||
final RestCallError cause = (RestCallError) e.getCause();
|
||||
if (cause.isUnexpectedError()) {
|
||||
log.error("Failed to execute action: {} | error: {} | cause: {}",
|
||||
PageAction.this.getName(),
|
||||
cause.getMessage(),
|
||||
Utils.getErrorCauseMessage(cause));
|
||||
}
|
||||
return Result.ofError(cause);
|
||||
}
|
||||
log.error("Failed to execute action: {} | error: {} | cause: {}",
|
||||
PageAction.this.getName(),
|
||||
e.getMessage(),
|
||||
Utils.getErrorCauseMessage(e));
|
||||
if (e.getCause() instanceof RestCallError) {
|
||||
return Result.ofError((RestCallError) e.getCause());
|
||||
}
|
||||
return Result.ofError(e);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to execute action: {} | error: {} | cause: {}",
|
||||
|
|
|
@ -20,6 +20,7 @@ import java.util.function.Supplier;
|
|||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.FeatureService;
|
||||
import org.eclipse.rap.rwt.RWT;
|
||||
import org.eclipse.swt.widgets.TreeItem;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -88,6 +89,7 @@ public class PageServiceImpl implements PageService {
|
|||
private final ResourceService resourceService;
|
||||
private final CurrentUser currentUser;
|
||||
private final ServerPushService serverPushService;
|
||||
private final FeatureService featureService;
|
||||
|
||||
public PageServiceImpl(
|
||||
final Cryptor cryptor,
|
||||
|
@ -96,7 +98,8 @@ public class PageServiceImpl implements PageService {
|
|||
final PolyglotPageService polyglotPageService,
|
||||
final ResourceService resourceService,
|
||||
final CurrentUser currentUser,
|
||||
final ServerPushService serverPushService) {
|
||||
final ServerPushService serverPushService,
|
||||
final FeatureService featureService) {
|
||||
|
||||
this.cryptor = cryptor;
|
||||
this.jsonMapper = jsonMapper;
|
||||
|
@ -105,6 +108,12 @@ public class PageServiceImpl implements PageService {
|
|||
this.resourceService = resourceService;
|
||||
this.currentUser = currentUser;
|
||||
this.serverPushService = serverPushService;
|
||||
this.featureService = featureService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeatureService getFeatureService() {
|
||||
return featureService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
@GuiProfile
|
||||
public class NewExam extends RestCall<Exam> {
|
||||
|
||||
public NewExam() {
|
||||
super(new TypeKey<>(
|
||||
CallType.NEW,
|
||||
EntityType.EXAM,
|
||||
new TypeReference<Exam>() {
|
||||
}),
|
||||
HttpMethod.POST,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_ADMINISTRATION_ENDPOINT);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam;
|
||||
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.FormBinding;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package ch.ethz.seb.sebserver.gui.widget;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.layout.GridLayout;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.DateTime;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
public class DateTimeSelector extends Composite {
|
||||
|
||||
private DateTime date;
|
||||
private DateTime time;
|
||||
private Label timeZoneLabel;
|
||||
private final DateTimeZone timeZone;
|
||||
private final String testKey;
|
||||
|
||||
public DateTimeSelector(
|
||||
final Composite parent,
|
||||
final WidgetFactory widgetFactory,
|
||||
final DateTimeZone timeZone,
|
||||
final String label,
|
||||
final String testKey) {
|
||||
|
||||
super(parent, SWT.NONE);
|
||||
this.timeZone = timeZone;
|
||||
this.testKey = testKey;
|
||||
|
||||
final GridLayout gridLayout = new GridLayout(3, false);
|
||||
gridLayout.verticalSpacing = 5;
|
||||
gridLayout.marginLeft = 0;
|
||||
gridLayout.marginHeight = 0;
|
||||
gridLayout.marginWidth = 0;
|
||||
setLayout(gridLayout);
|
||||
|
||||
this.date = widgetFactory.dateSelector(this, new LocTextKey(label), testKey);
|
||||
this.time = widgetFactory.timeSelector(this, new LocTextKey(label), testKey);
|
||||
this.timeZoneLabel = widgetFactory.label(this, timeZone.getID());
|
||||
|
||||
this.setValue(Utils.getMillisecondsNow());
|
||||
}
|
||||
|
||||
public void setValue(final long timestamp) {
|
||||
setDateTime(org.joda.time.DateTime.now(this.timeZone));
|
||||
}
|
||||
|
||||
public void setValue(final String dateTimeString) {
|
||||
if (dateTimeString == null) {
|
||||
setDateTime(org.joda.time.DateTime.now(this.timeZone));
|
||||
return;
|
||||
}
|
||||
setDateTime(Utils.toDateTime(dateTimeString).withZone(this.timeZone));
|
||||
}
|
||||
|
||||
public void setValue(final org.joda.time.DateTime time) {
|
||||
if (time == null) {
|
||||
setDateTime(org.joda.time.DateTime.now(this.timeZone));
|
||||
return;
|
||||
}
|
||||
setDateTime(time.withZone(this.timeZone));
|
||||
}
|
||||
|
||||
private void setDateTime(final org.joda.time.DateTime time) {
|
||||
this.date.setDate(time.getYear(), time.getMonthOfYear() - 1, time.getDayOfMonth());
|
||||
this.time.setTime(time.getHourOfDay(), time.getMinuteOfHour(), time.getSecondOfMinute());
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return Utils.formatDate(org.joda.time.DateTime.now(this.timeZone)
|
||||
.withYear(this.date.getYear())
|
||||
.withMonthOfYear(this.date.getMonth() + 1)
|
||||
.withDayOfMonth(this.date.getDay())
|
||||
.withHourOfDay((this.time != null) ? this.time.getHours() : 0)
|
||||
.withMinuteOfHour((this.time != null) ? this.time.getMinutes() : 0)
|
||||
.withSecondOfMinute((this.time != null) ? this.time.getSeconds() : 0));
|
||||
}
|
||||
|
||||
}
|
|
@ -151,7 +151,8 @@ public class WidgetFactory {
|
|||
NO_SHIELD("no_shield.png"),
|
||||
BACK("back.png"),
|
||||
SCREEN_PROC_ON("screen_proc_on.png"),
|
||||
SCREEN_PROC_OFF("screen_proc_off.png");
|
||||
SCREEN_PROC_OFF("screen_proc_off.png"),
|
||||
ADD_EXAM("add_exam.png");
|
||||
|
||||
public String fileName;
|
||||
private ImageData image = null;
|
||||
|
@ -882,6 +883,7 @@ public class WidgetFactory {
|
|||
public DateTime dateSelector(final Composite parent, final LocTextKey label, final String testKey) {
|
||||
RWT.setLocale(this.i18nSupport.getUsersFormatLocale());
|
||||
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
|
||||
gridData.widthHint = 120;
|
||||
final DateTime dateTime = new DateTime(parent, SWT.DATE | SWT.BORDER | SWT.DROP_DOWN);
|
||||
dateTime.setLayoutData(gridData);
|
||||
|
||||
|
|
|
@ -88,7 +88,12 @@ public class UserServiceImpl implements UserService {
|
|||
if (UserService.USERS_INSTITUTION_AS_DEFAULT.equals(text)) {
|
||||
setValue(getCurrentUser().institutionId());
|
||||
} else {
|
||||
setValue((text == null) ? null : Long.decode(text));
|
||||
try {
|
||||
setValue((text == null) ? null : Long.decode(text));
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to set institution from user: ", e);
|
||||
setValue(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -117,7 +117,7 @@ public interface AdditionalAttributesDAO {
|
|||
|
||||
/** Use this to save an additional attributes for a specific entity.
|
||||
* If an additional attribute with specified name already exists for the specified entity
|
||||
* this updates just the value for this additional attribute. Otherwise create a new instance
|
||||
* this updates just the value for this additional attribute. Otherwise, create a new instance
|
||||
* of additional attribute with the given data
|
||||
*
|
||||
* @param type the entity type
|
||||
|
|
|
@ -300,6 +300,9 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
.where(
|
||||
ExamRecordDynamicSqlSupport.active,
|
||||
isEqualTo(BooleanUtils.toInteger(true)))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.lmsSetupId,
|
||||
isNotNull())
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isNotEqualTo(ExamStatus.ARCHIVED.name()))
|
||||
|
|
|
@ -294,7 +294,10 @@ public class ExamRecordDAO {
|
|||
null, // active
|
||||
exam.examTemplateId,
|
||||
Utils.getMillisecondsNow(),
|
||||
null, null, null, null);
|
||||
exam.lmsSetupId == null ? exam.name : null,
|
||||
exam.lmsSetupId == null ? exam.startTime : null,
|
||||
exam.lmsSetupId == null ? exam.endTime : null,
|
||||
null);
|
||||
|
||||
this.examRecordMapper.updateByPrimaryKeySelective(examRecord);
|
||||
return this.examRecordMapper.selectByPrimaryKey(exam.id);
|
||||
|
|
|
@ -8,10 +8,15 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.*;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.validation.FieldError;
|
||||
|
@ -19,14 +24,9 @@ import org.springframework.validation.FieldError;
|
|||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroupData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroupData.ClientGroupType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroupData.ClientOS;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringService;
|
||||
|
@ -90,7 +90,7 @@ public interface ExamAdminService {
|
|||
|
||||
/** This indicates if proctoring is set and enabled for a certain exam.
|
||||
*
|
||||
* @param examId the exam instance
|
||||
* @param exam the exam instance
|
||||
* @return proctoring is enabled flag */
|
||||
default boolean isProctoringEnabled(final Exam exam) {
|
||||
if (exam == null || exam.id == null) {
|
||||
|
@ -107,7 +107,7 @@ public interface ExamAdminService {
|
|||
|
||||
/** This indicates if screen proctoring is set and enabled for a certain exam.
|
||||
*
|
||||
* @param examId the exam instance
|
||||
* @param exam the exam instance
|
||||
* @return screen proctoring is enabled flag */
|
||||
default boolean isScreenProctoringEnabled(final Exam exam) {
|
||||
if (exam == null || exam.id == null) {
|
||||
|
@ -163,10 +163,58 @@ public interface ExamAdminService {
|
|||
* @param exam the exam that has been changed and saved */
|
||||
void notifyExamSaved(Exam exam);
|
||||
|
||||
static void newExamFieldValidation(final POSTMapper postParams) {
|
||||
final Collection<APIMessage> validationErrors = new ArrayList<>();
|
||||
|
||||
if (!postParams.contains(Domain.EXAM.ATTR_QUIZ_NAME)) {
|
||||
validationErrors.add(APIMessage.fieldValidationError(
|
||||
Domain.EXAM.ATTR_QUIZ_NAME,
|
||||
"exam:quizName:notNull"));
|
||||
} else {
|
||||
final int length = postParams.getString(Domain.EXAM.ATTR_QUIZ_NAME).length();
|
||||
if (length < 3 || length > 255) {
|
||||
validationErrors.add(APIMessage.fieldValidationError(
|
||||
Domain.EXAM.ATTR_QUIZ_NAME,
|
||||
"exam:quizName:size:3:255:" + length));
|
||||
}
|
||||
}
|
||||
|
||||
if (!postParams.contains(QuizData.QUIZ_ATTR_START_URL)) {
|
||||
validationErrors.add(APIMessage.fieldValidationError(
|
||||
QuizData.QUIZ_ATTR_START_URL,
|
||||
"exam:quiz_start_url:notNull"));
|
||||
} else {
|
||||
try {
|
||||
new URL(postParams.getString(QuizData.QUIZ_ATTR_START_URL)).toURI();
|
||||
} catch (final Exception e) {
|
||||
validationErrors.add(APIMessage.fieldValidationError(
|
||||
QuizData.QUIZ_ATTR_START_URL,
|
||||
"exam:quiz_start_url:invalidURL"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!postParams.contains(Domain.EXAM.ATTR_QUIZ_START_TIME)) {
|
||||
validationErrors.add(APIMessage.fieldValidationError(
|
||||
Domain.EXAM.ATTR_QUIZ_START_TIME,
|
||||
"exam:quizStartTime:notNull"));
|
||||
} else if (postParams.contains(Domain.EXAM.ATTR_QUIZ_END_TIME)) {
|
||||
if (postParams.getDateTime(Domain.EXAM.ATTR_QUIZ_START_TIME)
|
||||
.isAfter(postParams.getDateTime(Domain.EXAM.ATTR_QUIZ_END_TIME))) {
|
||||
validationErrors.add(APIMessage.fieldValidationError(
|
||||
Domain.EXAM.ATTR_QUIZ_END_TIME,
|
||||
"exam:quizEndTime:endBeforeStart"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
throw new APIMessageException(validationErrors);
|
||||
}
|
||||
}
|
||||
|
||||
/** Used to check threshold consistency for a given list of thresholds.
|
||||
* Checks if all values are present (none null value)
|
||||
* Checks if there are duplicates
|
||||
*
|
||||
* <p>
|
||||
* If a check fails, the methods throws a APIMessageException with a FieldError to notify the caller
|
||||
*
|
||||
* @param thresholds List of Threshold */
|
||||
|
|
|
@ -110,9 +110,9 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter {
|
|||
builder.append(Constants.COMMA);
|
||||
builder.append(eventData.getNumericValue() != null ? eventData.getNumericValue() : "");
|
||||
builder.append(Constants.COMMA);
|
||||
builder.append(Utils.formatDate(Utils.toDateTimeUTC(eventData.getClientTime())));
|
||||
builder.append(Utils.formatDateWithMilliseconds(Utils.toDateTimeUTC(eventData.getClientTime())));
|
||||
builder.append(Constants.COMMA);
|
||||
builder.append(Utils.formatDate(Utils.toDateTimeUTC(eventData.getServerTime())));
|
||||
builder.append(Utils.formatDateWithMilliseconds(Utils.toDateTimeUTC(eventData.getServerTime())));
|
||||
|
||||
if (connectionData != null) {
|
||||
builder.append(Constants.COMMA);
|
||||
|
@ -129,9 +129,9 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter {
|
|||
builder.append(Constants.COMMA);
|
||||
builder.append(examData.getType().name());
|
||||
builder.append(Constants.COMMA);
|
||||
builder.append(Utils.formatDate(examData.getStartTime()));
|
||||
builder.append(Utils.formatDateWithMilliseconds(examData.getStartTime()));
|
||||
builder.append(Constants.COMMA);
|
||||
builder.append(Utils.formatDate(examData.getEndTime()));
|
||||
builder.append(Utils.formatDateWithMilliseconds(examData.getEndTime()));
|
||||
}
|
||||
|
||||
builder.append(Constants.CARRIAGE_RETURN);
|
||||
|
|
|
@ -242,17 +242,18 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
|
|||
log.debug("ExamDeletionEvent received, process releaseSEBClientRestriction...");
|
||||
}
|
||||
|
||||
event.ids.stream().forEach(examId -> {
|
||||
this.examDAO
|
||||
.byPK(examId)
|
||||
.onSuccess(exam -> {
|
||||
releaseSEBClientRestriction(exam)
|
||||
.onError(error -> log.error(
|
||||
"Failed to release SEB restrictions for finished exam: {}",
|
||||
exam,
|
||||
error));
|
||||
});
|
||||
});
|
||||
event.ids.stream().forEach(this::processExamDeletion);
|
||||
}
|
||||
|
||||
private Result<Exam> processExamDeletion(final Long examId) {
|
||||
return this.examDAO
|
||||
.byPK(examId)
|
||||
.whenDo(
|
||||
exam -> exam.lmsSetupId != null,
|
||||
exam -> releaseSEBClientRestriction(exam).getOrThrow()
|
||||
).onError(error -> log.error(
|
||||
"Failed to release SEB restrictions for finished exam: {}",
|
||||
examId, error));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -135,7 +135,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
protected SqlTable getSQLTableOfEntity() {
|
||||
return ExamRecordDynamicSqlSupport.examRecord;
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT,
|
||||
|
@ -173,11 +172,10 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
defaultValue = "false") final boolean includeRestriction) {
|
||||
|
||||
checkReadPrivilege(institutionId);
|
||||
final Collection<APIMessage> result = this.examSessionService
|
||||
|
||||
return this.examSessionService
|
||||
.checkExamConsistency(modelId)
|
||||
.getOrThrow();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
|
@ -596,15 +594,21 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
final SEBServerUser currentUser = this.authorization.getUserService().getCurrentUser();
|
||||
postParams.putIfAbsent(EXAM.ATTR_OWNER, currentUser.uuid());
|
||||
|
||||
return this.lmsAPIService
|
||||
.getLmsAPITemplate(lmsSetupId)
|
||||
.map(template -> {
|
||||
this.authorization.checkRead(template.lmsSetup());
|
||||
return template;
|
||||
})
|
||||
.flatMap(template -> template.getQuiz(quizId))
|
||||
.map(quiz -> new Exam(null, quiz, postParams))
|
||||
.getOrThrow();
|
||||
// NO LMS based exam is possible since v1.6
|
||||
if (quizId == null) {
|
||||
ExamAdminService.newExamFieldValidation(postParams);
|
||||
return new Exam(postParams);
|
||||
} else {
|
||||
return this.lmsAPIService
|
||||
.getLmsAPITemplate(lmsSetupId)
|
||||
.map(template -> {
|
||||
this.authorization.checkRead(template.lmsSetup());
|
||||
return template;
|
||||
})
|
||||
.flatMap(template -> template.getQuiz(quizId))
|
||||
.map(quiz -> new Exam(null, quiz, postParams))
|
||||
.getOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -764,11 +768,13 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
static Function<Collection<Exam>, List<Exam>> pageSort(final String sort) {
|
||||
|
||||
final String sortBy = PageSortOrder.decode(sort);
|
||||
return exams -> {
|
||||
final List<Exam> list = exams.stream().collect(Collectors.toList());
|
||||
final List<Exam> list = new ArrayList<>(exams);
|
||||
if (StringUtils.isBlank(sort)) {
|
||||
return list;
|
||||
}
|
||||
|
@ -789,5 +795,4 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
return list;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ sebserver.gui.list.page.size=15
|
|||
|
||||
sebserver.gui.multilingual=false
|
||||
sebserver.gui.supported.languages=en,de
|
||||
sebserver.gui.date.displayformat=de
|
||||
sebserver.gui.date.displayformat=en
|
||||
|
||||
sebserver.gui.seb.client.config.download.filename=SEBServerSettings.seb
|
||||
sebserver.gui.seb.exam.config.download.filename=SEBExamSettings.seb
|
|
@ -0,0 +1,4 @@
|
|||
-- -----------------------------------------------------
|
||||
-- Alter Table `exam`
|
||||
-- -----------------------------------------------------
|
||||
ALTER TABLE `exam` MODIFY `lms_setup_id` BIGINT UNSIGNED NULL;
|
|
@ -108,21 +108,17 @@ sebserver.form.validation.fieldError.invalidURL=The input does not match the URL
|
|||
sebserver.form.validation.fieldError.exists=This name already exists. Please choose another one
|
||||
sebserver.form.validation.fieldError.email=Invalid mail address
|
||||
sebserver.form.validation.fieldError.serverNotAvailable=No service seems to be available within the given URL
|
||||
<<<<<<< HEAD
|
||||
sebserver.form.validation.fieldError.url.invalid=Invalid URL. The given URL cannot be reached
|
||||
sebserver.form.validation.fieldError.typeInvalid=This type is not implemented yet and cannot be used
|
||||
sebserver.form.validation.fieldError.url.noservice=The expected service is not available within the given URL and API access
|
||||
=======
|
||||
sebserver.form.validation.fieldError.url.invalid=Invalid URL. The given URL cannot be reached.
|
||||
sebserver.form.validation.fieldError.typeInvalid=This type is not implemented yet and cannot be used.
|
||||
sebserver.form.validation.fieldError.url.noservice=The expected service is not available within the given URL and API access.
|
||||
sebserver.form.validation.fieldError.url.noaccess=There has no access been granted by the service. Please check the given access credentials.
|
||||
>>>>>>> refs/remotes/origin/rel-1.5.2
|
||||
sebserver.form.validation.fieldError.thresholdDuplicate=There are duplicate threshold values.
|
||||
sebserver.form.validation.fieldError.thresholdEmpty=There are missing values or colors for the threshold declaration
|
||||
sebserver.form.validation.fieldError.invalidIP=Invalid IP v4. Please enter a valid IP-address (v4)
|
||||
sebserver.form.validation.fieldError.invalidIPRange=Invalid IP-address range
|
||||
sebserver.form.validation.fieldError.url.noAccess=Access was denied
|
||||
sebserver.form.validation.fieldError.invalidDateRange=Invalid Date Range
|
||||
sebserver.form.validation.fieldError.endBeforeStart=Invalid Date Range, End before Start
|
||||
sebserver.error.unexpected=Unexpected Error
|
||||
sebserver.page.message=Information
|
||||
sebserver.dialog.confirm.title=Confirmation
|
||||
|
@ -552,6 +548,7 @@ sebserver.exam.action.list.hide.missing=Hide Missing Exams
|
|||
sebserver.exam.action.list.show.missing=Show Missing Exams
|
||||
sebserver.exam.action.modify=Edit Exam
|
||||
sebserver.exam.action.import=Import From Quizzes
|
||||
sebserver.exam.action.new=Add Exam Without LMS
|
||||
sebserver.exam.action.save=Save Exam
|
||||
sebserver.exam.action.activate=Activate Exam
|
||||
sebserver.exam.action.deactivate=Deactivate Exam
|
||||
|
@ -572,10 +569,10 @@ sebserver.exam.form.title=Exam
|
|||
sebserver.exam.form.title.subtitle=
|
||||
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=LMS exam Identifier
|
||||
sebserver.exam.form.quizid=Identifier
|
||||
sebserver.exam.form.quizid.tooltip=The identifier that identifies the quiz of the exam on the corresponding LMS
|
||||
sebserver.exam.form.quizurl=LMS exam URL
|
||||
sebserver.exam.form.quizurl.tooltip=The direct URL link to the LMS exam
|
||||
sebserver.exam.form.quizurl=Exam URL
|
||||
sebserver.exam.form.quizurl.tooltip=The direct URL link to the exam
|
||||
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
|
||||
|
|
BIN
src/main/resources/static/images/add_exam.png
Normal file
BIN
src/main/resources/static/images/add_exam.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 190 B |
|
@ -56,7 +56,7 @@ DROP TABLE IF EXISTS `exam` ;
|
|||
CREATE TABLE IF NOT EXISTS `exam` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`institution_id` BIGINT UNSIGNED NOT NULL,
|
||||
`lms_setup_id` BIGINT UNSIGNED NOT NULL,
|
||||
`lms_setup_id` BIGINT UNSIGNED NULL,
|
||||
`external_id` VARCHAR(255) NOT NULL,
|
||||
`owner` VARCHAR(255) NOT NULL,
|
||||
`supporter` VARCHAR(4000) NULL COMMENT 'comma separated list of user_uuid',
|
||||
|
|
Loading…
Reference in a new issue