/**
* <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.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.olat.admin.user.UserShortDescription;
import org.olat.core.dispatcher.mapper.Mapper;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
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.impl.FormBasicController;
import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
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.creator.ControllerCreator;
import org.olat.core.gui.media.MediaResource;
import org.olat.core.gui.media.NotFoundMediaResource;
import org.olat.core.id.Identity;
import org.olat.core.util.CodeHelper;
import org.olat.core.util.StringHelper;
import org.olat.course.assessment.AssessmentHelper;
import org.olat.fileresource.DownloadeableMediaResource;
import org.olat.fileresource.types.ImsQTI21Resource;
import org.olat.fileresource.types.ImsQTI21Resource.PathResourceLocator;
import org.olat.ims.qti21.AssessmentItemSession;
import org.olat.ims.qti21.AssessmentTestSession;
import org.olat.ims.qti21.QTI21AssessmentResultsOptions;
import org.olat.ims.qti21.QTI21Constants;
import org.olat.ims.qti21.QTI21Service;
import org.olat.ims.qti21.model.QTI21QuestionType;
import org.olat.ims.qti21.ui.assessment.TerminatedStaticCandidateSessionContext;
import org.olat.ims.qti21.ui.components.InteractionResultFormItem;
import org.olat.ims.qti21.ui.components.ItemBodyResultFormItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.w3c.dom.Document;
import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.DrawingInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.EndAttemptInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.ExtendedTextInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.PositionObjectInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.UploadInteraction;
import uk.ac.ed.ph.jqtiplus.node.result.AssessmentResult;
import uk.ac.ed.ph.jqtiplus.node.result.ItemResult;
import uk.ac.ed.ph.jqtiplus.node.result.ItemVariable;
import uk.ac.ed.ph.jqtiplus.node.result.OutcomeVariable;
import uk.ac.ed.ph.jqtiplus.node.result.SessionStatus;
import uk.ac.ed.ph.jqtiplus.node.result.TestResult;
import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef;
import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem;
import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest;
import uk.ac.ed.ph.jqtiplus.state.AssessmentSectionSessionState;
import uk.ac.ed.ph.jqtiplus.state.ControlObjectSessionState;
import uk.ac.ed.ph.jqtiplus.state.ItemSessionState;
import uk.ac.ed.ph.jqtiplus.state.TestPlan;
import uk.ac.ed.ph.jqtiplus.state.TestPlanNode;
import uk.ac.ed.ph.jqtiplus.state.TestPlanNode.TestNodeType;
import uk.ac.ed.ph.jqtiplus.state.TestPlanNodeKey;
import uk.ac.ed.ph.jqtiplus.state.TestSessionState;
import uk.ac.ed.ph.jqtiplus.state.marshalling.ItemSessionStateXmlMarshaller;
import uk.ac.ed.ph.jqtiplus.types.Identifier;
import uk.ac.ed.ph.jqtiplus.value.BooleanValue;
import uk.ac.ed.ph.jqtiplus.value.NumberValue;
import uk.ac.ed.ph.jqtiplus.value.Value;
import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator;
/**
*
* Initial date: 21.05.2015<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class AssessmentResultController extends FormBasicController {
private final String mapperUri;
private String signatureMapperUri;
private final String submissionMapperUri;
private final QTI21AssessmentResultsOptions options;
private final boolean anonym;
private final boolean withPrint;
private final boolean withTitle;
private final Identity assessedIdentity;
private final TestSessionState testSessionState;
private final AssessmentResult assessmentResult;
private final AssessmentTestSession candidateSession;
private final CandidateSessionContext candidateSessionContext;
private final File fUnzippedDirRoot;
private final URI assessmentObjectUri;
private final ResourceLocator inputResourceLocator;
private final ResolvedAssessmentTest resolvedAssessmentTest;
private UserShortDescription assessedIdentityInfosCtrl;
private final Map<String,AssessmentItemSession> identifierToItemSession = new HashMap<>();
private int count = 0;
@Autowired
private QTI21Service qtiService;
public AssessmentResultController(UserRequest ureq, WindowControl wControl, Identity assessedIdentity, boolean anonym,
AssessmentTestSession candidateSession, File fUnzippedDirRoot, String mapperUri, String submissionMapperUri,
QTI21AssessmentResultsOptions options, boolean withPrint, boolean withTitle) {
this(ureq, wControl, assessedIdentity, anonym, candidateSession, fUnzippedDirRoot, mapperUri, submissionMapperUri, options, withPrint, withTitle, null);
}
public AssessmentResultController(UserRequest ureq, WindowControl wControl, Identity assessedIdentity, boolean anonym,
AssessmentTestSession candidateSession, File fUnzippedDirRoot, String mapperUri, String submissionMapperUri,
QTI21AssessmentResultsOptions options, boolean withPrint, boolean withTitle, String exportUri) {
super(ureq, wControl, "assessment_results");
this.anonym = anonym;
this.options = options;
this.mapperUri = mapperUri;
this.withPrint = withPrint;
this.withTitle = withTitle;
this.assessedIdentity = assessedIdentity;
this.candidateSession = candidateSession;
this.fUnzippedDirRoot = fUnzippedDirRoot;
this.submissionMapperUri = submissionMapperUri;
ResourceLocator fileResourceLocator = new PathResourceLocator(fUnzippedDirRoot.toPath());
inputResourceLocator =
ImsQTI21Resource.createResolvingResourceLocator(fileResourceLocator);
assessmentObjectUri = qtiService.createAssessmentTestUri(fUnzippedDirRoot);
if(!anonym && assessedIdentity != null) {
assessedIdentityInfosCtrl = new UserShortDescription(ureq, getWindowControl(), assessedIdentity);
listenTo(assessedIdentityInfosCtrl);
}
resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(fUnzippedDirRoot, false, false);
File signature = qtiService.getAssessmentResultSignature(candidateSession);
if (signature != null) {
if (exportUri != null) {
signatureMapperUri = exportUri;
} else {
signatureMapperUri = registerCacheableMapper(ureq, "QTI21Signature::" + CodeHelper.getForeverUniqueID(),
new SignatureMapper(signature));
}
}
testSessionState = qtiService.loadTestSessionState(candidateSession);
assessmentResult = qtiService.getAssessmentResult(candidateSession);
candidateSessionContext = new TerminatedStaticCandidateSessionContext(candidateSession);
List<AssessmentItemSession> itemSessions = qtiService.getAssessmentItemSessions(candidateSession);
for(AssessmentItemSession itemSession:itemSessions) {
identifierToItemSession.put(itemSession.getAssessmentItemIdentifier(), itemSession);
}
initForm(ureq);
}
@Override
protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
if(formLayout instanceof FormLayoutContainer) {
FormLayoutContainer layoutCont = (FormLayoutContainer)formLayout;
layoutCont.contextPut("title", new Boolean(withTitle));
layoutCont.contextPut("print", new Boolean(withPrint));
layoutCont.contextPut("printCommand", Boolean.FALSE);
if(withPrint) {
layoutCont.contextPut("winid", "w" + layoutCont.getFormItemComponent().getDispatchID());
layoutCont.getFormItemComponent().addListener(this);
}
if(assessedIdentityInfosCtrl != null) {
layoutCont.put("assessedIdentityInfos", assessedIdentityInfosCtrl.getInitialComponent());
} else if(anonym) {
layoutCont.contextPut("anonym", Boolean.TRUE);
}
Results results = new Results(false, "o_qtiassessment_icon", options.isMetadata());
results.setSessionState(testSessionState);
layoutCont.contextPut("testResults", results);
TestResult testResult = assessmentResult.getTestResult();
if(testResult != null) {
extractOutcomeVariable(testResult.getItemVariables(), results);
}
if(signatureMapperUri != null) {
String signatureUrl = signatureMapperUri + "/assessmentResultSignature.xml";
layoutCont.contextPut("signatureUrl", signatureUrl);
}
initFormSections(layoutCont);
}
}
private void initFormSections(FormLayoutContainer layoutCont) {
List<Results> itemResults = new ArrayList<>();
layoutCont.contextPut("itemResults", itemResults);
Map<Identifier, AssessmentItemRef> identifierToRefs = new HashMap<>();
for(AssessmentItemRef itemRef:resolvedAssessmentTest.getAssessmentItemRefs()) {
identifierToRefs.put(itemRef.getIdentifier(), itemRef);
}
TestPlan testPlan = testSessionState.getTestPlan();
List<TestPlanNode> nodes = testPlan.getTestPlanNodeList();
for(TestPlanNode node:nodes) {
TestPlanNodeKey testPlanNodeKey = node.getKey();
TestNodeType testNodeType = node.getTestNodeType();
if(testNodeType == TestNodeType.ASSESSMENT_SECTION) {
Results r = new Results(true, node.getSectionPartTitle(), "o_mi_qtisection", options.isSectionSummary());
AssessmentSectionSessionState sectionState = testSessionState.getAssessmentSectionSessionStates().get(testPlanNodeKey);
if(sectionState != null) {
r.setSessionState(sectionState);
}
itemResults.add(r);
} else if(testNodeType == TestNodeType.ASSESSMENT_ITEM_REF) {
Results results = initFormItemResult(layoutCont, node, identifierToRefs);
if(results != null) {
itemResults.add(results);
}
}
}
}
private Results initFormItemResult(FormLayoutContainer layoutCont, TestPlanNode node, Map<Identifier, AssessmentItemRef> identifierToRefs) {
TestPlanNodeKey testPlanNodeKey = node.getKey();
Identifier identifier = testPlanNodeKey.getIdentifier();
AssessmentItemRef itemRef = identifierToRefs.get(identifier);
ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful();
QTI21QuestionType type = QTI21QuestionType.getType(assessmentItem);
AssessmentItemSession itemSession = identifierToItemSession.get(identifier.toString());
Results r = new Results(false, node.getSectionPartTitle(), type.getCssClass(), options.isQuestionSummary());
r.setSessionStatus("");//init
ItemSessionState sessionState = testSessionState.getItemSessionStates().get(testPlanNodeKey);
if(sessionState != null) {
r.setSessionState(sessionState);
SessionStatus sessionStatus = sessionState.getSessionStatus();
if(sessionState != null) {
r.setSessionStatus(translate("results.session.status." + sessionStatus.toQtiString()));
}
}
ItemResult itemResult = assessmentResult.getItemResult(identifier.toString());
if(itemResult != null) {
extractOutcomeVariable(itemResult.getItemVariables(), r);
}
if(itemSession != null) {
if(itemSession.getManualScore() != null) {
r.setScore(AssessmentHelper.getRoundedScore(itemSession.getManualScore()));
}
r.setComment(itemSession.getCoachComment());
}
if(options.isQuestions()) {
FormItem questionItem = initQuestionItem(layoutCont, sessionState, resolvedAssessmentItem);
r.setQuestionItem(questionItem);
}
if(options.isUserSolutions() || options.isCorrectSolutions()) {
List<InteractionResults> interactionResults = initFormItemInteractions(layoutCont, sessionState, assessmentItem, resolvedAssessmentItem);
r.getInteractionResults().addAll(interactionResults);
}
return r;
}
private FormItem initQuestionItem(FormLayoutContainer layoutCont, ItemSessionState sessionState,
ResolvedAssessmentItem resolvedAssessmentItem) {
FormItem responseFormItem = null;
if(options.isQuestions()) {
String responseId = "responseBody" + count++;
ItemBodyResultFormItem formItem = new ItemBodyResultFormItem(responseId, resolvedAssessmentItem);
Document clonedState = ItemSessionStateXmlMarshaller.marshal(sessionState);
ItemSessionState clonedSessionState = ItemSessionStateXmlMarshaller.unmarshal(clonedState.getDocumentElement());
clonedSessionState.resetResponses();
formItem.setItemSessionState(clonedSessionState);
formItem.setCandidateSessionContext(candidateSessionContext);
formItem.setResolvedAssessmentTest(resolvedAssessmentTest);
formItem.setResourceLocator(inputResourceLocator);
formItem.setAssessmentObjectUri(assessmentObjectUri);
formItem.setMapperUri(mapperUri);
layoutCont.add(responseId, formItem);
responseFormItem = formItem;
}
return responseFormItem;
}
private List<InteractionResults> initFormItemInteractions(FormLayoutContainer layoutCont, ItemSessionState sessionState,
AssessmentItem assessmentItem, ResolvedAssessmentItem resolvedAssessmentItem) {
//loop interactions, show response and solution
List<Interaction> interactions = assessmentItem.getItemBody().findInteractions();
List<InteractionResults> interactionResults = new ArrayList<>();
for(Interaction interaction:interactions) {
if(interaction instanceof PositionObjectInteraction || interaction instanceof EndAttemptInteraction) {
continue;
}
FormItem responseFormItem = null;
if(options.isUserSolutions()) {
//response
String responseId = "responseItem" + count++;
InteractionResultFormItem formItem = new InteractionResultFormItem(responseId, interaction, resolvedAssessmentItem);
initInteractionResultFormItem(formItem, sessionState);
layoutCont.add(responseId, formItem);
responseFormItem = formItem;
}
//solution
FormItem solutionFormItem = null;
if(interaction instanceof ExtendedTextInteraction || interaction instanceof UploadInteraction || interaction instanceof DrawingInteraction) {
// OO correct solution only for Word
} else if(options.isCorrectSolutions()) {
String solutionId = "solutionItem" + count++;
InteractionResultFormItem formItem = new InteractionResultFormItem(solutionId, interaction, resolvedAssessmentItem);
formItem.setShowSolution(true);
initInteractionResultFormItem(formItem, sessionState);
layoutCont.add(solutionId, formItem);
solutionFormItem = formItem;
}
interactionResults.add(new InteractionResults(responseFormItem, solutionFormItem));
}
return interactionResults;
}
private void initInteractionResultFormItem(InteractionResultFormItem formItem, ItemSessionState sectionState) {
formItem.setItemSessionState(sectionState);
formItem.setCandidateSessionContext(candidateSessionContext);
formItem.setResolvedAssessmentTest(resolvedAssessmentTest);
formItem.setResourceLocator(inputResourceLocator);
formItem.setAssessmentObjectUri(assessmentObjectUri);
formItem.setMapperUri(mapperUri);
if(submissionMapperUri != null) {
formItem.setSubmissionMapperUri(submissionMapperUri);
}
}
private void extractOutcomeVariable(List<ItemVariable> itemVariables, Results results) {
for(ItemVariable itemVariable:itemVariables) {
if(itemVariable instanceof OutcomeVariable) {
if(QTI21Constants.SCORE_IDENTIFIER.equals(itemVariable.getIdentifier())) {
results.setScore(getOutcomeNumberVariable(itemVariable));
} else if(QTI21Constants.MAXSCORE_IDENTIFIER.equals(itemVariable.getIdentifier())) {
results.setMaxScore(getOutcomeNumberVariable(itemVariable));
} else if(QTI21Constants.PASS_IDENTIFIER.equals(itemVariable.getIdentifier())) {
results.setPass(getOutcomeBooleanVariable(itemVariable));
}
}
}
}
private String getOutcomeNumberVariable(ItemVariable outcomeVariable) {
Value value = outcomeVariable.getComputedValue();
if(value instanceof NumberValue) {
return AssessmentHelper.getRoundedScore(((NumberValue)value).doubleValue());
}
return null;
}
private Boolean getOutcomeBooleanVariable(ItemVariable outcomeVariable) {
Value value = outcomeVariable.getComputedValue();
if(value instanceof BooleanValue) {
return ((BooleanValue)value).booleanValue();
}
return null;
}
@Override
protected void doDispose() {
//
}
@Override
protected void formOK(UserRequest ureq) {
//
}
@Override
public void event(UserRequest ureq, Component source, Event event) {
if(flc.getFormItemComponent() == source && "print".equals(event.getCommand())) {
doPrint(ureq);
}
super.event(ureq, source, event);
}
private void doPrint(UserRequest ureq) {
ControllerCreator creator = new ControllerCreator() {
@Override
public Controller createController(UserRequest uureq, WindowControl wwControl) {
AssessmentResultController printViewCtrl = new AssessmentResultController(uureq, wwControl, assessedIdentity, anonym,
candidateSession, fUnzippedDirRoot, mapperUri, submissionMapperUri, options, false, true);
printViewCtrl.flc.contextPut("printCommand", Boolean.TRUE);
listenTo(printViewCtrl);
return printViewCtrl;
}
};
openInNewBrowserWindow(ureq, creator);
}
public static class InteractionResults {
private final FormItem responseFormItem;
private final FormItem solutionFormItem;
public InteractionResults(FormItem responseFormItem, FormItem solutionFormItem) {
this.responseFormItem = responseFormItem;
this.solutionFormItem = solutionFormItem;
}
public FormItem getResponseFormItem() {
return responseFormItem;
}
public FormItem getSolutionFormItem() {
return solutionFormItem;
}
}
public static class Results {
private Date entryTime;
private Date endTime;
private Long duration;
private String score;
private String maxScore;
private Boolean pass;
private String comment;
private final String title;
private final String cssClass;
private final boolean section;
private final boolean metadataVisible;
private String sessionStatus;
private FormItem questionItem;
private final List<InteractionResults> interactionResults = new ArrayList<>();
public Results(boolean section, String cssClass, boolean metadataVisible) {
this.section = section;
this.cssClass = cssClass;
this.metadataVisible = metadataVisible;
this.title = null;
}
public Results(boolean section, String title, String cssClass, boolean metadataVisible) {
this.section = section;
this.title = title;
this.cssClass = cssClass;
this.metadataVisible = metadataVisible;
}
public void setSessionState(ControlObjectSessionState sessionState) {
entryTime = sessionState.getEntryTime();
endTime = sessionState.getEndTime();
duration = sessionState.getDurationAccumulated();
}
public boolean isMetadataVisible() {
return metadataVisible;
}
public boolean hasInteractions() {
for(InteractionResults interactionResult:interactionResults) {
if(interactionResult.getResponseFormItem() != null || interactionResult.getSolutionFormItem() != null) {
return true;
}
}
return false;
}
public String getCssClass() {
return cssClass;
}
public String getTitle() {
return title;
}
public boolean isSection() {
return section;
}
public Date getEntryTime() {
return entryTime;
}
public Date getEndTime() {
return endTime;
}
public Long getDuration() {
return duration;
}
public boolean hasScore() {
return StringHelper.containsNonWhitespace(score);
}
public String getScore() {
return score;
}
public void setScore(String score) {
this.score = score;
}
public boolean hasMaxScore() {
return StringHelper.containsNonWhitespace(maxScore);
}
public String getMaxScore() {
return maxScore;
}
public void setMaxScore(String maxScore) {
this.maxScore = maxScore;
}
public boolean hasPass() {
return pass != null;
}
public Boolean getPass() {
return pass;
}
public void setPass(Boolean pass) {
this.pass = pass;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getSessionStatus() {
return sessionStatus == null ? "" : sessionStatus;
}
public void setSessionStatus(String sessionStatus) {
this.sessionStatus = sessionStatus;
}
public FormItem getQuestionItem() {
return questionItem;
}
public void setQuestionItem(FormItem questionItem) {
this.questionItem = questionItem;
}
public List<InteractionResults> getInteractionResults() {
return interactionResults;
}
}
public class SignatureMapper implements Mapper {
private final File signature;
public SignatureMapper(File signature) {
this.signature = signature;
}
@Override
public MediaResource handle(String relPath, HttpServletRequest request) {
MediaResource resource;
if(signature.exists()) {
resource = new DownloadeableMediaResource(signature);
} else {
resource = new NotFoundMediaResource(relPath);
}
return resource;
}
}
}