/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.ims.qti21.ui;
import java.io.File;
import java.math.BigDecimal;
import java.net.URI;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import org.olat.core.commons.persistence.DB;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.EscapeMode;
import org.olat.core.gui.components.form.flexible.FormItem;
import org.olat.core.gui.components.form.flexible.FormItemContainer;
import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement;
import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
import org.olat.core.gui.components.form.flexible.impl.FormEvent;
import org.olat.core.gui.components.form.flexible.impl.elements.table.BooleanCellRenderer;
import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel;
import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory;
import org.olat.core.gui.components.form.flexible.impl.elements.table.SelectionEvent;
import org.olat.core.gui.components.form.flexible.impl.elements.table.StaticFlexiCellRenderer;
import org.olat.core.gui.components.form.flexible.impl.elements.table.TextFlexiCellRenderer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
import org.olat.core.gui.control.generic.modal.DialogBoxController;
import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.resource.OresHelper;
import org.olat.course.CourseFactory;
import org.olat.course.nodes.IQTESTCourseNode;
import org.olat.course.nodes.iq.IQEditController;
import org.olat.course.nodes.iq.QTI21AssessmentRunController;
import org.olat.course.run.environment.CourseEnvironment;
import org.olat.course.run.scoring.ScoreEvaluation;
import org.olat.course.run.userview.UserCourseEnvironment;
import org.olat.fileresource.FileResourceManager;
import org.olat.ims.qti21.AssessmentSessionAuditLogger;
import org.olat.ims.qti21.AssessmentTestSession;
import org.olat.ims.qti21.QTI21AssessmentResultsOptions;
import org.olat.ims.qti21.QTI21DeliveryOptions;
import org.olat.ims.qti21.QTI21Module;
import org.olat.ims.qti21.QTI21Service;
import org.olat.ims.qti21.model.DigitalSignatureOptions;
import org.olat.ims.qti21.ui.QTI21TestSessionTableModel.TSCols;
import org.olat.ims.qti21.ui.assessment.IdentityAssessmentTestCorrectionController;
import org.olat.ims.qti21.ui.event.RetrieveAssessmentTestSessionEvent;
import org.olat.modules.ModuleConfiguration;
import org.olat.modules.assessment.AssessmentEntry;
import org.olat.modules.assessment.AssessmentService;
import org.olat.modules.assessment.AssessmentToolOptions;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.RepositoryManager;
import org.olat.repository.model.RepositoryEntrySecurity;
import org.olat.user.UserManager;
import org.springframework.beans.factory.annotation.Autowired;
/**
* This controller is used by the assessment tools of the course and
* of the test resource. The assessment tool of the resource doesn't
* provide any user course environment or course node. Be aware!
*
*
* Initial date: 28.06.2016<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class QTI21AssessmentDetailsController extends FormBasicController {
private Component resetToolCmp;
private FlexiTableElement tableEl;
private QTI21TestSessionTableModel tableModel;
private RepositoryEntry entry;
private RepositoryEntry testEntry;
private final String subIdent;
private final boolean manualCorrections;
private final Identity assessedIdentity;
private final boolean readOnly;
private final IQTESTCourseNode courseNode;
private final RepositoryEntrySecurity reSecurity;
private final UserCourseEnvironment assessedUserCourseEnv;
private CloseableModalController cmc;
private AssessmentResultController resultCtrl;
private QTI21ResetToolController resetToolCtrl;
private DialogBoxController retrieveConfirmationCtr;
private IdentityAssessmentTestCorrectionController correctionCtrl;
@Autowired
private DB dbInstance;
@Autowired
private UserManager userManager;
@Autowired
private QTI21Module qtiModule;
@Autowired
protected QTI21Service qtiService;
@Autowired
private RepositoryManager repositoryManager;
@Autowired
private AssessmentService assessmentService;
/**
* The constructor used by the assessment tool of the course.
*
* @param ureq
* @param wControl
* @param assessableEntry
* @param courseNode
* @param coachCourseEnv
* @param assessedUserCourseEnv
*/
public QTI21AssessmentDetailsController(UserRequest ureq, WindowControl wControl,
RepositoryEntry assessableEntry, IQTESTCourseNode courseNode,
UserCourseEnvironment coachCourseEnv, UserCourseEnvironment assessedUserCourseEnv) {
super(ureq, wControl, "assessment_details");
entry = assessableEntry;
this.courseNode = courseNode;
subIdent = courseNode.getIdent();
readOnly = coachCourseEnv.isCourseReadOnly();
this.assessedUserCourseEnv = assessedUserCourseEnv;
testEntry = courseNode.getReferencedRepositoryEntry();
assessedIdentity = assessedUserCourseEnv.getIdentityEnvironment().getIdentity();
manualCorrections = qtiService.needManualCorrection(testEntry);
RepositoryEntry courseEntry = assessedUserCourseEnv.getCourseEnvironment().getCourseGroupManager().getCourseEntry();
reSecurity = repositoryManager.isAllowed(ureq, courseEntry);
initForm(ureq);
updateModel();
}
/**
* The constructor used by the assessment tool of the test resource itself.
*
* @param ureq
* @param wControl
* @param assessableEntry
* @param assessedIdentity
*/
public QTI21AssessmentDetailsController(UserRequest ureq, WindowControl wControl,
RepositoryEntry assessableEntry, Identity assessedIdentity) {
super(ureq, wControl, "assessment_details");
entry = assessableEntry;
testEntry = assessableEntry;
subIdent = null;
readOnly = false;
courseNode = null;
assessedUserCourseEnv = null;
this.assessedIdentity = assessedIdentity;
manualCorrections = qtiService.needManualCorrection(assessableEntry);
reSecurity = repositoryManager.isAllowed(ureq, assessableEntry);
initForm(ureq);
updateModel();
}
@Override
protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel();
columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(TSCols.terminationTime));
columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(TSCols.lastModified));
columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(TSCols.duration, new TextFlexiCellRenderer(EscapeMode.none)));
columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(TSCols.results, new TextFlexiCellRenderer(EscapeMode.none)));
if(readOnly) {
columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel("select", translate("select"), "open"));
} else {
columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(TSCols.open.i18nHeaderKey(), TSCols.open.ordinal(), "open",
new BooleanCellRenderer(new StaticFlexiCellRenderer(translate("select"), "open"),
new StaticFlexiCellRenderer(translate("pull"), "open"))));
}
if(manualCorrections && !readOnly) {
columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(TSCols.correction.i18nHeaderKey(), TSCols.correction.ordinal(), "correction",
new BooleanCellRenderer(new StaticFlexiCellRenderer(translate("correction"), "correction"), null)));
}
tableModel = new QTI21TestSessionTableModel(columnsModel, getTranslator());
tableEl = uifactory.addTableElement(getWindowControl(), "sessions", tableModel, 20, false, getTranslator(), formLayout);
tableEl.setEmtpyTableMessageKey("results.empty");
if(reSecurity.isEntryAdmin() && !readOnly) {
AssessmentToolOptions asOptions = new AssessmentToolOptions();
asOptions.setAdmin(reSecurity.isEntryAdmin());
asOptions.setIdentities(Collections.singletonList(assessedIdentity));
if(courseNode != null) {
resetToolCtrl = new QTI21ResetToolController(ureq, getWindowControl(),
assessedUserCourseEnv.getCourseEnvironment(), asOptions, courseNode);
} else {
resetToolCtrl = new QTI21ResetToolController(ureq, getWindowControl(), entry, asOptions);
}
listenTo(resetToolCtrl);
resetToolCmp = resetToolCtrl.getInitialComponent();
}
}
@Override
protected void doDispose() {
//
}
protected void updateModel() {
List<AssessmentTestSession> sessions = qtiService.getAssessmentTestSessions(entry, subIdent, assessedIdentity);
Collections.sort(sessions, new AssessmentTestSessionComparator());
tableModel.setObjects(sessions);
tableEl.reloadData();
tableEl.reset();
if(resetToolCmp != null) {
if(sessions.size() > 0) {
flc.getFormItemComponent().put("reset.tool", resetToolCmp);
} else {
flc.getFormItemComponent().remove(resetToolCmp);
}
}
}
@Override
protected void event(UserRequest ureq, Controller source, Event event) {
if(cmc == source) {
cleanUp();
} else if(correctionCtrl == source) {
if(event == Event.DONE_EVENT || event == Event.CHANGED_EVENT) {
if(courseNode != null) {
doUpdateCourseNode(correctionCtrl.getAssessmentTestSession());
} else {
doUpdateEntry(correctionCtrl.getAssessmentTestSession());
}
updateModel();
fireEvent(ureq, Event.CHANGED_EVENT);
}
cmc.deactivate();
cleanUp();
} else if(retrieveConfirmationCtr == source) {
if(DialogBoxUIFactory.isYesEvent(event) || DialogBoxUIFactory.isOkEvent(event)) {
doPullSession((AssessmentTestSession)retrieveConfirmationCtr.getUserObject());
updateModel();
}
} else if(resetToolCtrl == source) {
if(event == Event.DONE_EVENT) {
updateModel();
fireEvent(ureq, Event.CHANGED_EVENT);
}
}
super.event(ureq, source, event);
}
private void cleanUp() {
removeAsListenerAndDispose(correctionCtrl);
removeAsListenerAndDispose(resultCtrl);
removeAsListenerAndDispose(cmc);
correctionCtrl = null;
resultCtrl = null;
cmc = null;
}
@Override
protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
if(tableEl == source) {
if(event instanceof SelectionEvent) {
SelectionEvent se = (SelectionEvent)event;
String cmd = se.getCommand();
AssessmentTestSession row = tableModel.getObject(se.getIndex());
row = qtiService.getAssessmentTestSession(row.getKey());
if("open".equals(cmd)) {
if(row.getFinishTime() == null) {
doConfirmPullSession(ureq, row);
} else {
doOpenResult(ureq, row);
}
} else if("correction".equals(cmd)) {
doCorrection(ureq, row);
}
}
}
super.formInnerEvent(ureq, source, event);
}
@Override
protected void formOK(UserRequest ureq) {
//
}
private void doCorrection(UserRequest ureq, AssessmentTestSession session) {
correctionCtrl = new IdentityAssessmentTestCorrectionController(ureq, getWindowControl(), session);
listenTo(correctionCtrl);
cmc = new CloseableModalController(getWindowControl(), "close", correctionCtrl.getInitialComponent(),
true, translate("correction"));
cmc.activate();
listenTo(cmc);
}
private void doUpdateCourseNode(AssessmentTestSession session) {
ScoreEvaluation scoreEval = courseNode.getUserScoreEvaluation(assessedUserCourseEnv);
BigDecimal finalScore = calculateFinalScore(session);
Float score = finalScore == null ? null : finalScore.floatValue();
ScoreEvaluation manualScoreEval = new ScoreEvaluation(score, scoreEval.getPassed(),
scoreEval.getAssessmentStatus(), null, scoreEval.getFullyAssessed(), session.getKey());
courseNode.updateUserScoreEvaluation(manualScoreEval, assessedUserCourseEnv, getIdentity(), false);
}
private void doUpdateEntry(AssessmentTestSession session) {
AssessmentEntry assessmentEntry = assessmentService.loadAssessmentEntry(assessedIdentity, entry, null, entry);
BigDecimal finalScore = calculateFinalScore(session);
assessmentEntry.setScore(finalScore);
assessmentEntry.setAssessmentId(session.getKey());
assessmentService.updateAssessmentEntry(assessmentEntry);
}
private BigDecimal calculateFinalScore(AssessmentTestSession session) {
BigDecimal finalScore = session.getScore();
if(finalScore == null) {
finalScore = session.getManualScore();
} else if(session.getManualScore() != null) {
finalScore = finalScore.add(session.getManualScore());
}
return finalScore;
}
private void doConfirmPullSession(UserRequest ureq, AssessmentTestSession session) {
String title = translate("pull");
String fullname = userManager.getUserDisplayName(session.getIdentity());
String text = translate("retrievetest.confirm.text", new String[]{ fullname });
retrieveConfirmationCtr = activateOkCancelDialog(ureq, title, text, retrieveConfirmationCtr);
retrieveConfirmationCtr.setUserObject(session);
}
private void doPullSession(AssessmentTestSession session) {
session = qtiService.getAssessmentTestSession(session.getKey());
if(session.getFinishTime() == null) {
if(qtiModule.isDigitalSignatureEnabled()) {
qtiService.signAssessmentResult(session, getSignatureOptions(session), session.getIdentity());
}
session.setFinishTime(new Date());
}
session.setTerminationTime(new Date());
session = qtiService.updateAssessmentTestSession(session);
dbInstance.commit();//make sure that the changes committed before sending the event
AssessmentSessionAuditLogger candidateAuditLogger = qtiService.getAssessmentSessionAuditLogger(session, false);
candidateAuditLogger.logTestRetrieved(session, getIdentity());
OLATResourceable sessionOres = OresHelper.createOLATResourceableInstance(AssessmentTestSession.class, session.getKey());
CoordinatorManager.getInstance().getCoordinator().getEventBus()
.fireEventToListenersOf(new RetrieveAssessmentTestSessionEvent(session.getKey()), sessionOres);
}
private DigitalSignatureOptions getSignatureOptions(AssessmentTestSession session) {
RepositoryEntry sessionTestEntry = session.getTestEntry();
QTI21DeliveryOptions deliveryOptions = qtiService.getDeliveryOptions(sessionTestEntry);
boolean digitalSignature = deliveryOptions.isDigitalSignature();
boolean sendMail = deliveryOptions.isDigitalSignatureMail();
if(courseNode != null) {
ModuleConfiguration config = courseNode.getModuleConfiguration();
digitalSignature = config.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE,
deliveryOptions.isDigitalSignature());
sendMail = config.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE_SEND_MAIL,
deliveryOptions.isDigitalSignatureMail());
}
DigitalSignatureOptions options = new DigitalSignatureOptions(digitalSignature, sendMail, entry, testEntry);
if(digitalSignature) {
if(courseNode == null) {
AssessmentEntryOutcomesListener.decorateResourceConfirmation(session, options, null, getLocale());
} else {
CourseEnvironment courseEnv = CourseFactory.loadCourse(entry).getCourseEnvironment();
QTI21AssessmentRunController.decorateCourseConfirmation(session, options, courseEnv, courseNode, sessionTestEntry, null, getLocale());
}
}
return options;
}
private void doOpenResult(UserRequest ureq, AssessmentTestSession session) {
if(resultCtrl != null) return;
FileResourceManager frm = FileResourceManager.getInstance();
File fUnzippedDirRoot = frm.unzipFileResource(session.getTestEntry().getOlatResource());
URI assessmentObjectUri = qtiService.createAssessmentTestUri(fUnzippedDirRoot);
File submissionDir = qtiService.getSubmissionDirectory(session);
String mapperUri = registerCacheableMapper(ureq, "QTI21DetailsResources::" + session.getKey(),
new ResourcesMapper(assessmentObjectUri, submissionDir));
resultCtrl = new AssessmentResultController(ureq, getWindowControl(), assessedIdentity, false, session,
fUnzippedDirRoot, mapperUri, null, QTI21AssessmentResultsOptions.allOptions(), true, true);
listenTo(resultCtrl);
cmc = new CloseableModalController(getWindowControl(), "close", resultCtrl.getInitialComponent(),
true, translate("table.header.results"));
cmc.activate();
listenTo(cmc);
}
public static class AssessmentTestSessionComparator implements Comparator<AssessmentTestSession> {
@Override
public int compare(AssessmentTestSession a1, AssessmentTestSession a2) {
Date t1 = a1.getTerminationTime();
if(t1 == null) {
t1 = a1.getFinishTime();
}
Date t2 = a2.getTerminationTime();
if(t2 == null) {
t2 = a2.getFinishTime();
}
int c;
if(t1 == null && t2 == null) {
c = 0;
} else if(t2 == null) {
return -1;
} else if(t1 == null) {
return 1;
} else {
c = t1.compareTo(t2);
}
if(c == 0) {
Date c1 = a1.getCreationDate();
Date c2 = a2.getCreationDate();
if(c1 == null && c2 == null) {
c = 0;
} else if(c2 == null) {
return -1;
} else if(c1 == null) {
return 1;
} else {
c = c1.compareTo(c2);
}
}
return -c;
}
}
}