fixes code cleanup and docu

This commit is contained in:
anhefti 2019-06-06 13:29:25 +02:00
parent f53243e001
commit 4e4883b8b7
19 changed files with 169 additions and 34 deletions

View file

@ -49,21 +49,19 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@GuiProfile
public class ExamList implements TemplateComposer {
private final PageService pageService;
private final ResourceService resourceService;
private final int pageSize;
private final static LocTextKey emptySelectionTextKey =
private static final LocTextKey NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION =
new LocTextKey("sebserver.exam.list.action.no.modify.privilege");
private final static LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.exam.info.pleaseSelect");
private final static LocTextKey columnTitleLmsSetupKey =
private final static LocTextKey COLUMN_TITLE_KEY =
new LocTextKey("sebserver.exam.list.column.lmssetup");
private final static LocTextKey columnTitleNameKey =
private final static LocTextKey COLUMN_TITLE_NAME_KEY =
new LocTextKey("sebserver.exam.list.column.name");
private final static LocTextKey columnTitleTypeKey =
private final static LocTextKey COLUMN_TITLE_TYPE_KEY =
new LocTextKey("sebserver.exam.list.column.type");
private final static LocTextKey noModifyOfOutDatedExams =
private final static LocTextKey NO_MODIFY_OF_OUT_DATED_EXAMS =
new LocTextKey("sebserver.exam.list.modify.out.dated");
private final static LocTextKey emptyListTextKey =
private final static LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.exam.list.empty");
private final TableFilterAttribute lmsFilter;
@ -72,6 +70,10 @@ public class ExamList implements TemplateComposer {
private final TableFilterAttribute startTimeFilter =
new TableFilterAttribute(CriteriaType.DATE, QuizData.FILTER_ATTR_START_TIME);
private final PageService pageService;
private final ResourceService resourceService;
private final int pageSize;
protected ExamList(
final PageService pageService,
final ResourceService resourceService,
@ -105,17 +107,17 @@ public class ExamList implements TemplateComposer {
// table
final EntityTable<Exam> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetExamPage.class))
.withEmptyMessage(emptyListTextKey)
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(this.pageSize)
.withColumn(new ColumnDefinition<>(
Domain.EXAM.ATTR_LMS_SETUP_ID,
columnTitleLmsSetupKey,
COLUMN_TITLE_KEY,
examLmsSetupNameFunction(this.resourceService))
.withFilter(this.lmsFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_NAME,
columnTitleNameKey,
COLUMN_TITLE_NAME_KEY,
Exam::getName)
.withFilter(this.nameFilter)
.sortable())
@ -129,7 +131,7 @@ public class ExamList implements TemplateComposer {
.sortable())
.withColumn(new ColumnDefinition<>(
Domain.EXAM.ATTR_TYPE,
columnTitleTypeKey,
COLUMN_TITLE_TYPE_KEY,
this::examTypeName)
.sortable())
.withDefaultAction(actionBuilder
@ -142,17 +144,17 @@ public class ExamList implements TemplateComposer {
actionBuilder
.newAction(ActionDefinition.EXAM_IMPORT)
.publishIf(userGrant::im) // TODO iw instead of im?
.publishIf(userGrant::im)
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelection, emptySelectionTextKey)
.withSelect(table::getSelection, PageAction::applySingleSelection, EMPTY_SELECTION_TEXT_KEY)
.publishIf(table::hasAnyContent)
.newAction(ActionDefinition.EXAM_MODIFY_FROM_LIST)
.withSelect(
table::getSelection,
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION),
action -> this.modifyExam(action, table),
emptySelectionTextKey)
EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im() && table.hasAnyContent());
}
@ -163,7 +165,7 @@ public class ExamList implements TemplateComposer {
if (exam.startTime != null) {
final DateTime now = DateTime.now(DateTimeZone.UTC);
if (exam.startTime.isBefore(now)) {
throw new PageMessageException(noModifyOfOutDatedExams);
throw new PageMessageException(NO_MODIFY_OF_OUT_DATED_EXAMS);
}
}

View file

@ -45,6 +45,8 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@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 =
@ -151,7 +153,9 @@ public class LmsSetupList implements TemplateComposer {
.publishIf(() -> table.hasAnyContent())
.newAction(ActionDefinition.LMS_SETUP_MODIFY_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelection, EMPTY_SELECTION_TEXT_KEY)
.withSelect(
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION),
PageAction::applySingleSelection, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im() && table.hasAnyContent());
}

View file

@ -47,6 +47,8 @@ import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
@GuiProfile
public class SebClientConfigList implements TemplateComposer {
private static final LocTextKey NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION =
new LocTextKey("sebserver.clientconfig.list.action.no.modify.privilege");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.clientconfig.list.empty");
private static final LocTextKey TITLE_TEXT_KEY =
@ -155,7 +157,9 @@ public class SebClientConfigList implements TemplateComposer {
.publishIf(() -> table.hasAnyContent())
.newAction(ActionDefinition.SEB_CLIENT_CONFIG_MODIFY_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelection, EMPTY_SELECTION_TEXT_KEY)
.withSelect(
table.getGrantedSelection(this.currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION),
PageAction::applySingleSelection, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> clientConfigGrant.im() && table.hasAnyContent());
}

View file

@ -41,6 +41,8 @@ import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
@GuiProfile
public class SebExamConfigList implements TemplateComposer {
private static final LocTextKey NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION =
new LocTextKey("sebserver.examconfig.list.action.no.modify.privilege");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.examconfig.list.empty");
private static final LocTextKey TITLE_TEXT_KEY =
@ -148,11 +150,15 @@ public class SebExamConfigList implements TemplateComposer {
.publishIf(() -> table.hasAnyContent())
.newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY_PROP_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelection, EMPTY_SELECTION_TEXT_KEY)
.withSelect(
table.getGrantedSelection(this.currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION),
PageAction::applySingleSelection, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> examConfigGrant.im() && table.hasAnyContent())
.newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelection, EMPTY_SELECTION_TEXT_KEY)
.withSelect(
table.getGrantedSelection(this.currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUION),
PageAction::applySingleSelection, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> examConfigGrant.im() && table.hasAnyContent());
}

View file

@ -13,7 +13,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
@ -90,10 +89,6 @@ public class PassworFieldBuilder implements InputFieldBuilder {
return;
}
if (StringUtils.isBlank(pwd) && StringUtils.isBlank(confirm)) {
return;
}
if (!pwd.equals(confirm)) {
passwordInputField.showError(viewContext
.getI18nSupport()

View file

@ -17,6 +17,8 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.eclipse.swt.SWT;
@ -35,14 +37,17 @@ import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.ImageIcon;
@ -257,6 +262,10 @@ public class EntityTable<ROW extends Entity> {
}
public Set<EntityKey> getSelection() {
return getSelection(null);
}
public Set<EntityKey> getSelection(final Predicate<ROW> grantCheck) {
final TableItem[] selection = this.table.getSelection();
if (selection == null) {
return Collections.emptySet();
@ -264,10 +273,27 @@ public class EntityTable<ROW extends Entity> {
return Arrays.asList(selection)
.stream()
.filter(item -> grantCheck == null || grantCheck.test(getRowData(item)))
.map(this::getRowDataId)
.collect(Collectors.toSet());
}
public Supplier<Set<EntityKey>> getGrantedSelection(
final CurrentUser currentUser,
final LocTextKey denyMessage) {
return () -> getSelection(e -> {
if (!(e instanceof GrantEntity)) {
return true;
}
if (currentUser.entityGrantCheck((GrantEntity) e).m()) {
return true;
} else {
throw new PageMessageException(denyMessage);
}
});
}
private void createTableColumns() {
for (final ColumnDefinition<ROW> column : this.columns) {
final TableColumn tableColumn = this.widgetFactory.tableColumnLocalized(

View file

@ -80,7 +80,7 @@ public class AuthorizationServiceImpl implements AuthorizationService {
// grants for seb client config
addPrivilege(EntityType.SEB_CLIENT_CONFIGURATION)
.forRole(UserRole.SEB_SERVER_ADMIN)
.withBasePrivilege(PrivilegeType.WRITE)
.withBasePrivilege(PrivilegeType.READ)
.andForRole(UserRole.INSTITUTIONAL_ADMIN)
.withInstitutionalPrivilege(PrivilegeType.WRITE)
.andForRole(UserRole.EXAM_ADMIN)

View file

@ -80,12 +80,21 @@ public interface BulkActionSupportDAO<T extends Entity> {
throw new UnsupportedOperationException("Unsupported Bulk Action: " + bulkAction);
}
/** This creates a collection of Results refer the given entity keys.
*
* @param keys Collection of entity keys to create Results from
* @return a collection of Results refer the given entity keys. */
static Collection<Result<EntityKey>> transformResult(final Collection<EntityKey> keys) {
return keys.stream()
.map(key -> Result.of(key))
.collect(Collectors.toList());
}
/** This creates a list of Result refer to a given error for all given EntityKey instances.
*
* @param error the error that shall be referred by created Result's
* @param all all entity keys to create error Result for
* @return List of Result refer to a given error for all given EntityKey instances */
static List<Result<EntityKey>> handleBulkActionError(final Throwable error, final Set<EntityKey> all) {
return all.stream()
.map(key -> Result.<EntityKey> ofError(new BulkActionEntityException(key)))
@ -97,7 +106,7 @@ public interface BulkActionSupportDAO<T extends Entity> {
* and applies the selection functions for each, collecting the resulting dependency EntityKeys
* into one Set of all dependency keys for all source keys
*
*
*
* @param bulkAction The BulkAction that defines the source keys
* @param selectionFunction a selection functions that gives all dependency keys for a given source key
* @return */

View file

@ -15,6 +15,10 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ConfigurationAttributeDAO extends EntityDAO<ConfigurationAttribute, ConfigurationAttribute> {
/** Use this to get all ConfigurationAttribute that are root attributes and no child
* attributes (has no parent reference).
*
* @return Collection of all ConfigurationAttribute that are root attributes */
Result<Collection<ConfigurationAttribute>> getAllRootAttributes();
}

View file

@ -20,6 +20,12 @@ public final class DAOLoggingSupport {
public static final Logger log = LoggerFactory.getLogger(DAOLoggingSupport.class);
/** Use this as a functional method on Result processing to
* log an error that is referenced by a given Result and skip further processing by
* using Result.skipOnError.
*
* @param result The given Result
* @return Stream of the results value or empty stream on error case */
public static <T> Stream<T> logAndSkipOnError(final Result<T> result) {
return Result.skipOnError(
result.onError(error -> log.error("Unexpected error. Object processing is skipped: ", error)));

View file

@ -174,7 +174,7 @@ public interface EntityDAO<T extends Entity, M extends ModelIdAware> {
* This uses the EntityType defined by this instance to filter all EntityKey by the given type and
* convert the matching EntityKey's to id's (PK's)
*
* Use this if you need to transform a Collection of EntityKey into a extracted List of id's of a specified
* Use this if you need to transform a Collection of EntityKey into a extracted Set of id's of a specified
* EntityType
*
* @param keys Collection of EntityKey of various types
@ -198,6 +198,15 @@ public interface EntityDAO<T extends Entity, M extends ModelIdAware> {
}
}
/** Context based utility method to extract a set of id's (PK) from a collection of various EntityKey
* This uses the EntityType defined by this instance to filter all EntityKey by the given type and
* convert the matching EntityKey's to id's (PK's)
*
* Use this if you need to transform a Collection of EntityKey into a extracted List of id's of a specified
* EntityType
*
* @param keys Collection of EntityKey of various types
* @return List of id's (PK's) from the given key collection that match the concrete EntityType */
default List<Long> extractListOfPKs(final Collection<EntityKey> keys) {
return new ArrayList<>(extractPKsFromKeys(keys));
}

View file

@ -12,6 +12,7 @@ import org.joda.time.DateTime;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
@ -177,7 +178,7 @@ public class FilterMap extends POSTMapper {
}
public static String toSQLWildcard(final String text) {
return (text == null) ? null : "%" + text + "%";
return (text == null) ? null : Constants.PERCENTAGE + text + Constants.PERCENTAGE;
}
}

View file

@ -16,6 +16,10 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
public interface OrientationDAO extends EntityDAO<Orientation, Orientation> {
/** Use this to delete all Orientation of a defined template.
*
* @param templateId the template identifier (PK)
* @return Collection of all EntityKey of Orientations that has been deleted */
Result<Collection<EntityKey>> deleteAllOfTemplate(Long templateId);
}

View file

@ -48,7 +48,7 @@ public interface LmsAPIService {
/** This can be used to test an LmsSetup connection parameter without saving or heaving
* an already persistent version of an LmsSetup.
*
*
* @param lmsSetup
* @return */
LmsSetupTestResult testAdHoc(LmsSetup lmsSetup);
@ -76,7 +76,6 @@ public interface LmsAPIService {
return q -> {
final boolean nameFilter = StringUtils.isBlank(name) || (q.name != null && q.name.contains(name));
final boolean startTimeFilter = (from == null) || (q.startTime != null && q.startTime.isAfter(from));
// final boolean endTimeFilter = (now == null) || (q.endTime != null && q.endTime.isAfter(now));
return nameFilter && startTimeFilter /* && endTimeFilter */;
};
}

View file

@ -12,16 +12,32 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
/** Defines a validator for validating ConfigurationValue model instances */
public interface ConfigurationValueValidator {
public static final String MESSAGE_VALUE_OBJECT_NAME = "examConfigValue";
/** The name of the validator.
* Can be used within the validator field of an ConfigurationAttribute (SQL: configuration_attribute table)
* to force a Validator to validate attribute values of a certain ConfigurationAttribute.
*
* @return name of the validator */
String name();
/** Indicates if a ConfigurationValue is validated by this concrete validator.
*
* @param value ConfigurationValue instance
* @param attribute ConfigurationAttribute instance
* @return */
boolean validate(
ConfigurationValue value,
ConfigurationAttribute attribute);
/** Default convenient method to handle validation exception if validation failed.
*
* @param value ConfigurationValue instance
* @param attribute ConfigurationAttribute instance
* @throws FieldValidationException the FieldValidationException that is created and thrown */
default void throwValidationError(
final ConfigurationValue value,
final ConfigurationAttribute attribute) throws FieldValidationException {
@ -31,6 +47,11 @@ public interface ConfigurationValueValidator {
this.createErrorMessage(value, attribute));
}
/** Default convenient method to to create an error message in case of validation failure.
*
* @param value ConfigurationValue instance
* @param attribute ConfigurationAttribute instance
* @return error message */
default String createErrorMessage(
final ConfigurationValue value,
final ConfigurationAttribute attribute) {

View file

@ -38,16 +38,39 @@ public interface SebClientConfigService {
" </dict>\r\n" +
" </dict>\r\n";
/** Get the server URL prefix in form of;
* [scheme{http|https}]://[server-address{DNS-name|IP}]:[port]
*
* E.g.: https://seb.server.ch:8080
*
* @return the server URL prefix */
String getServerURL();
/** Indicates if there is any SebClientConfiguration for a specified institution.
*
* @param institutionId the institution identifier
* @return true if there is any SebClientConfiguration for a specified institution. False otherwise */
boolean hasSebClientConfigurationForInstitution(Long institutionId);
/** Use this to auto-generate a SebClientConfiguration for a specified institution.
* clientName and clientSecret are randomly generated.
*
* @param institutionId the institution identifier
* @return the created SebClientConfig */
Result<SebClientConfig> autoCreateSebClientConfigurationForInstitution(Long institutionId);
/** Use this to export a specified SebClientConfiguration within a given OutputStream.
*
* @param out OutputStream to write the export to
* @param modelId the model identifier of the SebClientConfiguration to export */
void exportSebClientConfiguration(
OutputStream out,
final String modelId);
/** Use this to get a encoded clientSecret for the SebClientConfiguration with specified clientId/clientName.
*
* @param clientId the clientId/clientName
* @return encoded clientSecret for that SebClientConfiguration with clientId or null of not existing */
Result<String> getEncodedClientSecret(String clientId);
}

View file

@ -41,6 +41,13 @@ public interface SebConfigCryptor {
final InputStream input,
final SebConfigEncryptionContext context);
/** Decrypt an incoming cipher data stream to an outgoing plain text data stream
*
* IMPORTANT: This must run in a separated thread
*
* @param output the output stream to write the plain text data to
* @param input the input stream to read the cipher text from
* @param context the SebConfigEncryptionContext to access strategy specific data needed for encryption */
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void decrypt(
final OutputStream output,

View file

@ -52,11 +52,21 @@ public interface SebConfigEncryptionService {
}
/** This can be used to stream incoming plain text data to encrypted cipher data output stream.
*
* @param output the output data stream to write the cipher text to
* @param input the input stream to read the plain text from
* @param context the SebConfigEncryptionContext to access strategy specific data needed for encryption */
void streamEncrypted(
final OutputStream output,
final InputStream input,
SebConfigEncryptionContext context);
/** This can be used to stream incoming cipher data to decrypted plain text data output stream.
*
* @param output the output data stream to write encrypted plain text to
* @param input the input stream to read the cipher text from
* @param context the SebConfigEncryptionContext to access strategy specific data needed for encryption */
void streamDecrypted(
final OutputStream output,
final InputStream input,

View file

@ -163,6 +163,7 @@ sebserver.lmssetup.type.MOODLE=Moodle
sebserver.lmssetup.type.OPEN_EDX=Open edX
sebserver.lmssetup.list.actions=Selected LMS Setup
sebserver.lmssetup.list.action.no.modify.privilege=No Access: A LMS Setup from other institution cannot be modified.
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
@ -170,6 +171,7 @@ sebserver.lmssetup.list.column.name=Name
sebserver.lmssetup.list.column.type=LMS Type
sebserver.lmssetup.list.column.active=Active
sebserver.lmssetup.action.list=LMS Setup
sebserver.lmssetup.action.form=LMS Setup
sebserver.lmssetup.action.new=New LMS Setup
@ -243,6 +245,7 @@ sebserver.exam.list.column.type=Type
sebserver.exam.list.empty=No Exams has been found. Please adapt the filter or import one from Quiz
sebserver.exam.list.modify.out.dated=Running or finished exams cannot be modified.
sebserver.exam.list.action.no.modify.privilege=No Access: An Exam from other institution cannot be modified.
sebserver.exam.action.list=Exam
sebserver.exam.action.list.view=View Exam
@ -348,6 +351,7 @@ sebserver.clientconfig.list.column.name=Name
sebserver.clientconfig.list.column.date=Creation Date
sebserver.clientconfig.list.column.active=Active
sebserver.clientconfig.info.pleaseSelect=Please Select a client configuration first
sebserver.clientconfig.list.action.no.modify.privilege=No Access: A SEB Client Configuration from other institution cannot be modified.
sebserver.clientconfig.form.title.new=New Client Configuration
sebserver.clientconfig.form.title=SEB Client Configuration
@ -379,6 +383,7 @@ sebserver.examconfig.list.actions=Selected Configuration
sebserver.examconfig.list.empty=There is currently no SEB-Exam configuration available. Please create a new one
sebserver.examconfig.info.pleaseSelect=Please Select an exam configuration first
sebserver.examconfig.list.action.no.modify.privilege=No Access: An Exam Configuration from other institution cannot be modified.
sebserver.examconfig.action.list.new=New Exam Configuration
sebserver.examconfig.action.list.view=View Configuration