Merge remote-tracking branch 'origin/dev-1.4' into development

This commit is contained in:
anhefti 2022-07-21 16:37:53 +02:00
commit 8cce1ee0e8
23 changed files with 191 additions and 103 deletions

View file

@ -151,6 +151,29 @@ your institution use the type information of the exam to set them into context.
- When you have selected a exam configuration the dialog shows you some additional information about the exam configuration.
- If you want or need to put an password protected encryption to the exam configuration for this exam you can do so by give the password for the encryption also within the attachment dialog. Be aware that every SEB client that will receive an encrypted exam configuration from the SEB Server will prompt the user to give the correct password. In most cases an encryption of the exam configuration is not needed, because a secure HTTPS connection form SEB client to SEB Server is already in place.
**Archive an exam**
Since SEB Server version 1.4 it is possible to archive an exam that has been finished. An archived exam and all its data is still available
on the SEB Server but read only and the exam is not been updated from the LMS data anymore and it is not possible to run this exam again.
This is a good use-case to organize your exams since archived exam are not shown in the Exam list with the default filter anymore. They are
only shown if the status filter of the exam list is explicitly set to Archived status. An they are shown within the new "Finished Exam"
section in the monitoring view.
.. image:: images/exam/archiveExamsFilter.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/exam/archiveExamsFilter.png
This is also a good use-case if you want to remove an LMS and LMS Setup but still want to be able to access the exams data on the SEB Server.
In this case you can archive all exams from that LMS Setup before deactivating or deleting the respective LMS Setup.
To archive a finished exam you just have to use the "Archive Exam" action on the right action pane of the exam view:
.. image:: images/exam/archiveExam1.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/exam/archiveExam1.png
**Delete an exam**
If you have "Exam Administrator" privileges you are able to entirely delete an existing exam and its dependencies.

View file

@ -55,6 +55,14 @@ And you are able to add/edit/remove monitoring indicators for the exam template
.. image:: images/exam_template/indicator.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/exam_template/indicator.png
There are also proctoring settings available since SEB Server version 1.4 for the exam template. They just have the same settings and
look like the ones on the Exam and will get copied for an exam imported with the respective template that defines the proctoring settings.
.. image:: images/exam_template/proctoringSettings.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/exam_template/proctoringSettings.png
Import Exam with Template
-------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -181,7 +181,39 @@ A Student as well as a proctor is then able to use all the features of the meeti
- In Zoom it is not possible to fully control a participant microphone. Therefore it may happen that participant can hear each other even if no proctor is in the meeting.
- Within Jitsi Meet service when a proctor leaves the room it currently happens that a random participant became host/moderator since it is not possible in Jitsi Meet to have a meeting without host. We try to mitigate the problem with the `moderator plugin <https://github.com/nvonahsen/jitsi-token-moderation-plugin>`_ or `Jitsi Meet SaS <https://jaas.8x8.vc/#/>`_
- In both services while broadcasting, it is not guaranteed that a student always see the proctor. Usually the meeting service shows or pins the participant that is currently speaking automatically.
Finished Exams
--------------
Since SEB Server version 1.4 there is a new section "Finished Exams" within the monitoring section to view finished and archived exams
like you do within the monitoring. You see all the SEB connections that has been connected to the exam when running and are able to view
particular SEB client connection details by either double-click on a SEB client connection entry in the list or by selection and using the View action
on the right action pane.
In the "Finished Exams" list you can see all finished or archived exams and filter the list by Name, State and Type.
.. image:: images/monitoring/finishedExams.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/monitoring/finishedExams.png
To see a particular finished or archived exam you can just double-click in the list entry or use the View action on the right action pane.
In the exam view you see all SEB connections that has been connected to the exam during the exam run just like in the usual monitoring view
but with no update since the SEB connections are not active and the data is not changing anymore. You are able to filter the list by
User or Session Info, Connection Info or Status and are also be able to sort the list even for indicator columns.
.. image:: images/monitoring/finishedExam.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/monitoring/finishedExam.png
As in the usual monitoring view, you can show SEB client connection details by double-clicking on a list entry or by selecting a list entry
and use the View action on the right action pane.
In the detail view you see the same information for a particular SEB client connection as within the usual monitoring view. You can view
the SEB client logs of a SEB client connection here and analyze it after the exam was running.
.. image:: images/monitoring/finishedClientConnection.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/monitoring/finishedClientConnection.png
All SEB Client Logs

View file

@ -166,7 +166,7 @@ Since SEB Server version 1.4, multi-selection for some lists with bulk-actions i
just click on the row as usual. If you then click on another (still not selected) row, this row get selected too. You can do this even over several pages.
To deselect a selected row just click it again then it will be removed from the selection.
.. image:: images/overview/list.png
.. image:: images/overview/list_multiselect.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/overview/list_multiselect.png
@ -204,4 +204,20 @@ After correcting the missing or wrong input and saveing the form again, the SEB
.. note::
If you navigate away from a form in edit mode, the GUI will inform you about possible data loss on this action and will prompt you to
proceed or abort the action.
proceed or abort the action.
**Actions**
Actions are usually placed on the right action pane of the application and belongs to the actual site or view. There are generally three types of actions:
- Form Actions that directly belongs to the actual view or object and either save, manipulate or create a new object.
- List Action - Single Selection are actions on a list page that effects the selected list entry.
- List Action - Multi Selection are actions that refer to the current multi selection on a list and apply for every selected item.
.. note::
List action are disabled when nothing is selected from the list and get enabled as soon as one or more list items are selected.
Actions that are considdered single selection actions, and are used with a multi selection on the list will only affect the first selected item in the list.
.. image:: images/overview/list_multiselect_actions.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/overview/list_multiselect_actions.png

View file

@ -6,7 +6,29 @@ There shall be at least a problem description, an optional explanation if needed
Please also have a look at `Open Issues <https://https://github.com/SafeExamBrowser/seb-server/issues>`_ and/or `Ongoing Discussions <https://github.com/SafeExamBrowser/seb-server/discussions>`_ on the Git-Hub page.
--------------------------------
- **Version** : 1.3.x
- **Domain** : Exam Monitoring
- **Problem** : SEB connections get lost and ping-times go up for already connected SEB clients
- **Explanation** : This issue is due to a access token used by SEB client to authenticate on SEB Server that lasts not longer the one hour since SEB Server version 1.3 and since SEB client has no new access token request implements yet.
- **Solution** : A workaround for SEB Server version 1.3.x is to make the access token expiry-date last long enough to minimize the possibility that the access token became invalid during a exam. We recommend to set it to 12 hours = 43200 seconds. Therefore please set the following SEB Server setup properties in the respective application-prod.properties configuration file of your SEB Server setup:
sebserver.webservice.api.admin.accessTokenValiditySeconds=43200
sebserver.webservice.api.exam.accessTokenValiditySeconds=43200
In SEB Server version 1.4 this is already set as default again and we are currently working on new SEB client versions that also
handle SEB Server communication token expiry by automatically requesting a new access token (with new lifetime) from SEB Server
when an old access token is not valid any longer.
--------------------------------
**Template**
--------------------------------
- **Version** : 1.0.0

View file

@ -30,7 +30,6 @@ import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
@ -89,7 +88,7 @@ public class ExamList implements TemplateComposer {
private final TableFilterAttribute institutionFilter;
private final TableFilterAttribute lmsFilter;
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
new TableFilterAttribute(CriteriaType.TEXT, Domain.EXAM.ATTR_QUIZ_NAME);
private final TableFilterAttribute stateFilter;
private final TableFilterAttribute typeFilter;
@ -177,21 +176,21 @@ public class ExamList implements TemplateComposer {
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_NAME,
Domain.EXAM.ATTR_QUIZ_NAME,
COLUMN_TITLE_NAME_KEY,
Exam::getName)
.withFilter(this.nameFilter)
.sortable())
.withColumn(new ColumnDefinition<>(
QuizData.QUIZ_ATTR_START_TIME,
Domain.EXAM.ATTR_QUIZ_START_TIME,
new LocTextKey(
EXAM_LIST_COLUMN_START_TIME,
i18nSupport.getUsersTimeZoneTitleSuffix()),
Exam::getStartTime)
.withFilter(new TableFilterAttribute(
CriteriaType.DATE,
QuizData.FILTER_ATTR_START_TIME,
Domain.EXAM.ATTR_QUIZ_START_TIME,
Utils.toDateTimeUTC(Utils.getMillisecondsNow())
.minusYears(1)
.toString()))

View file

@ -119,9 +119,14 @@ public interface PaginationService {
final List<T> sorted = pageFunction.apply(all);
final int _pageNumber = getPageNumber(pageNumber);
int _pageNumber = getPageNumber(pageNumber);
final int _pageSize = getPageSize(pageSize);
final int start = (_pageNumber - 1) * _pageSize;
int start = (_pageNumber - 1) * _pageSize;
if (start >= sorted.size()) {
start = 0;
_pageNumber = 1;
}
int end = start + _pageSize;
if (sorted.size() < end) {
end = sorted.size();

View file

@ -282,10 +282,11 @@ public class PaginationServiceImpl implements PaginationService {
final Map<String, String> examTableMap = new HashMap<>();
examTableMap.put(Entity.FILTER_ATTR_INSTITUTION, institutionNameRef);
examTableMap.put(Domain.EXAM.ATTR_LMS_SETUP_ID, lmsSetupNameRef);
// NOTE: This seems not to work and I was not able to figure out why.
// Now the type sorting is done within secondary sort for exams.
//examTableMap.put(Domain.EXAM.ATTR_TYPE, "'" + ExamRecordDynamicSqlSupport.type.name() + "'");
examTableMap.put(Domain.EXAM.ATTR_QUIZ_NAME, ExamRecordDynamicSqlSupport.quizName.name());
examTableMap.put(Domain.EXAM.ATTR_QUIZ_START_TIME, ExamRecordDynamicSqlSupport.quizStartTime.name());
examTableMap.put(Domain.EXAM.ATTR_QUIZ_END_TIME, ExamRecordDynamicSqlSupport.quizEndTime.name());
examTableMap.put(Domain.EXAM.ATTR_STATUS, ExamRecordDynamicSqlSupport.status.name());
examTableMap.put(Domain.EXAM.ATTR_TYPE, ExamRecordDynamicSqlSupport.type.name());
this.sortColumnMapping.put(ExamRecordDynamicSqlSupport.examRecord.name(), examTableMap);
this.defaultSortColumn.put(ExamRecordDynamicSqlSupport.examRecord.name(), Domain.EXAM.ATTR_ID);

View file

@ -19,6 +19,7 @@ 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.Domain;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
@ -108,7 +109,7 @@ public class FilterMap extends POSTMapper {
}
public DateTime getExamFromTime() {
return Utils.toDateTime(getString(QuizData.FILTER_ATTR_START_TIME));
return Utils.toDateTime(getString(Domain.EXAM.ATTR_QUIZ_START_TIME));
}
public DateTime getSEBClientConfigFromTime() {

View file

@ -25,7 +25,6 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.mybatis.dynamic.sql.update.UpdateDSL;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
@ -121,42 +120,16 @@ public class ExamDAOImpl implements ExamDAO {
return Result.tryCatch(() -> {
final Predicate<Exam> examDataFilter = createPredicate(filterMap);
return this.examRecordDAO
.allMatching(filterMap, null)
.flatMap(this::toDomainModel)
.getOrThrow()
.stream()
.filter(examDataFilter.and(predicate))
.filter(predicate)
.collect(Collectors.toList());
});
}
private Predicate<Exam> createPredicate(final FilterMap filterMap) {
final String name = filterMap.getQuizName();
final DateTime from = filterMap.getExamFromTime();
final Predicate<Exam> quizDataFilter = exam -> {
if (StringUtils.isNotBlank(name)) {
if (!exam.name.contains(name)) {
return false;
}
}
if (from != null && exam.startTime != null) {
// always show exams that has not ended yet
if (exam.endTime == null || exam.endTime.isAfter(from)) {
return true;
}
if (exam.startTime.isBefore(from)) {
return false;
}
}
return true;
};
return quizDataFilter;
}
@Override
public Result<Exam> updateState(final Long examId, final ExamStatus status, final String updateId) {
return this.examRecordDAO
@ -268,13 +241,12 @@ public class ExamDAOImpl implements ExamDAO {
.stream().map(s -> s.name())
.collect(Collectors.toList())
: null;
final Predicate<Exam> examDataFilter = createPredicate(filterMap);
return this.examRecordDAO
.allMatching(filterMap, stateNames)
.flatMap(this::toDomainModel)
.getOrThrow()
.stream()
.filter(examDataFilter.and(predicate))
.filter(predicate)
.collect(Collectors.toList());
});
}

View file

@ -172,8 +172,6 @@ public class ExamRecordDAO {
ExamRecordDynamicSqlSupport.active,
isEqualToWhenPresent(filterMap.getActiveAsInt()));
//
whereClause = whereClause
.and(
ExamRecordDynamicSqlSupport.institutionId,
@ -204,8 +202,15 @@ public class ExamRecordDAO {
isNotEqualTo(ExamStatus.ARCHIVED.name()));
}
final List<ExamRecord> records = whereClause
if (filterMap.getExamFromTime() != null) {
whereClause = whereClause
.and(
ExamRecordDynamicSqlSupport.quizEndTime,
isGreaterThanOrEqualToWhenPresent(filterMap.getExamFromTime()),
or(ExamRecordDynamicSqlSupport.quizEndTime, isNull()));
}
final List<ExamRecord> records = whereClause
.and(
ExamRecordDynamicSqlSupport.quizName,
isLikeWhenPresent(filterMap.getSQLWildcard(EXAM.ATTR_QUIZ_NAME)))
@ -465,20 +470,20 @@ public class ExamRecordDAO {
.execute();
// check those in not running state (and not archived) and are within the time-frame or on wrong side of the time-frame
// if finished but up-coming
// if finished but up-coming or running
final SqlCriterion<String> finished = or(
ExamRecordDynamicSqlSupport.status,
isEqualTo(ExamStatus.FINISHED.name()),
and(
ExamRecordDynamicSqlSupport.quizStartTime,
ExamRecordDynamicSqlSupport.quizEndTime,
SqlBuilder.isGreaterThanOrEqualToWhenPresent(now.plus(leadTime))));
// if up-coming but finished
// if up-coming but running or finished
final SqlCriterion<String> upcoming = or(
ExamRecordDynamicSqlSupport.status,
isEqualTo(ExamStatus.UP_COMING.name()),
and(
ExamRecordDynamicSqlSupport.quizEndTime,
ExamRecordDynamicSqlSupport.quizStartTime,
SqlBuilder.isLessThanWhenPresent(now.minus(followupTime))),
finished);

View file

@ -17,13 +17,12 @@ import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@ -39,14 +38,13 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Domain.EXAM;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
@ -120,50 +118,56 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return ExamRecordDynamicSqlSupport.examRecord;
}
@RequestMapping(
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@Override
public Page<Exam> getPage(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@RequestParam(name = Page.ATTR_PAGE_NUMBER, required = false) final Integer pageNumber,
@RequestParam(name = Page.ATTR_PAGE_SIZE, required = false) final Integer pageSize,
@RequestParam(name = Page.ATTR_SORT, required = false) final String sort,
@RequestParam final MultiValueMap<String, String> allRequestParams,
final HttpServletRequest request) {
checkReadPrivilege(institutionId);
this.authorization.check(
PrivilegeType.READ,
EntityType.EXAM,
institutionId);
if (StringUtils.isBlank(sort) ||
(this.paginationService.isNativeSortingSupported(ExamRecordDynamicSqlSupport.examRecord, sort))) {
return super.getPage(institutionId, pageNumber, pageSize, sort, allRequestParams, request);
} else {
final Collection<Exam> exams = this.examDAO
.allMatching(new FilterMap(
allRequestParams,
request.getQueryString()),
this::hasReadAccess)
.getOrThrow();
return this.paginationService.buildPageFromList(
pageNumber,
pageSize,
sort,
exams,
pageSort(sort));
}
}
// @RequestMapping(
// method = RequestMethod.GET,
// consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
// produces = MediaType.APPLICATION_JSON_VALUE)
// @Override
// public Page<Exam> getPage(
// @RequestParam(
// name = API.PARAM_INSTITUTION_ID,
// required = true,
// defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
// @RequestParam(name = Page.ATTR_PAGE_NUMBER, required = false) final Integer pageNumber,
// @RequestParam(name = Page.ATTR_PAGE_SIZE, required = false) final Integer pageSize,
// @RequestParam(name = Page.ATTR_SORT, required = false) final String sort,
// @RequestParam final MultiValueMap<String, String> allRequestParams,
// final HttpServletRequest request) {
//
// checkReadPrivilege(institutionId);
// this.authorization.check(
// PrivilegeType.READ,
// EntityType.EXAM,
// institutionId);
//
// if (StringUtils.isBlank(sort) ||
// (this.paginationService.isNativeSortingSupported(ExamRecordDynamicSqlSupport.examRecord, sort))) {
//
// System.out.println("*********************** sort, filter on DB");
//
// return super.getPage(institutionId, pageNumber, pageSize, sort, allRequestParams, request);
//
// } else {
//
// System.out.println("*********************** sort, filter on List");
//
// return super.getPage(institutionId, pageNumber, pageSize, sort, allRequestParams, request);
//
//// final Collection<Exam> exams = this.examDAO
//// .allMatching(new FilterMap(
//// allRequestParams,
//// request.getQueryString()),
//// this::hasReadAccess)
//// .getOrThrow();
////
//// return this.paginationService.buildPageFromList(
//// pageNumber,
//// pageSize,
//// sort,
//// exams,
//// pageSort(sort));
// }
// }
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
@ -586,13 +590,13 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
}
if (sortBy.equals(Exam.FILTER_ATTR_NAME) || sortBy.equals(QuizData.QUIZ_ATTR_NAME)) {
list.sort(Comparator.comparing(exam -> exam.name));
list.sort(Comparator.comparing(exam -> (exam.name != null) ? exam.name : StringUtils.EMPTY));
}
if (sortBy.equals(Exam.FILTER_ATTR_TYPE)) {
list.sort(Comparator.comparing(exam -> exam.type));
list.sort(Comparator.comparing(exam -> (exam.type != null) ? exam.type : ExamType.UNDEFINED));
}
if (sortBy.equals(QuizData.FILTER_ATTR_START_TIME) || sortBy.equals(QuizData.QUIZ_ATTR_START_TIME)) {
list.sort(Comparator.comparing(exam -> exam.startTime));
list.sort(Comparator.comparing(exam -> (exam.startTime != null) ? exam.startTime : new DateTime(0)));
}
if (PageSortOrder.DESCENDING == PageSortOrder.getSortOrder(sort)) {