/**
* <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.assessment;
import java.io.File;
import java.math.BigDecimal;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.olat.core.gui.UserRequest;
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.FormLink;
import org.olat.core.gui.components.form.flexible.elements.TextElement;
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.FormLayoutContainer;
import org.olat.core.gui.components.link.Link;
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.util.StringHelper;
import org.olat.course.assessment.AssessmentHelper;
import org.olat.fileresource.FileResourceManager;
import org.olat.fileresource.types.ImsQTI21Resource;
import org.olat.fileresource.types.ImsQTI21Resource.PathResourceLocator;
import org.olat.ims.qti21.AssessmentItemSession;
import org.olat.ims.qti21.AssessmentSessionAuditLogger;
import org.olat.ims.qti21.AssessmentTestHelper;
import org.olat.ims.qti21.AssessmentTestSession;
import org.olat.ims.qti21.QTI21Service;
import org.olat.ims.qti21.model.ParentPartItemRefs;
import org.olat.ims.qti21.model.xml.QtiNodesExtractor;
import org.olat.ims.qti21.ui.ResourcesMapper;
import org.olat.ims.qti21.ui.components.InteractionResultFormItem;
import org.olat.repository.RepositoryEntry;
import org.olat.user.UserManager;
import org.springframework.beans.factory.annotation.Autowired;
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.UploadInteraction;
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.ItemSessionState;
import uk.ac.ed.ph.jqtiplus.state.TestPlanNode;
import uk.ac.ed.ph.jqtiplus.state.TestPlanNodeKey;
import uk.ac.ed.ph.jqtiplus.state.TestSessionState;
import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator;
/**
*
* Initial date: 16.08.2016<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class IdentitiesAssessmentItemCorrectionController extends FormBasicController {
private FormLink nextQuestionButton;
private int count;
private final String mapperUri;
private final URI assessmentObjectUri;
private final ResourcesMapper resourcesMapper;
private final ResourceLocator inputResourceLocator;
private final AssessmentItemRef itemRef;
private final AssessmentItem assessmentItem;
private final List<Interaction> interactions;
private final ResolvedAssessmentItem resolvedAssessmentItem;
private final ResolvedAssessmentTest resolvedAssessmentTest;
private final AssessmentTestCorrection testCorrections;
private Map<Long, File> submissionDirectoryMaps = new HashMap<>();
private final List<IdentityAssessmentItemWrapper> itemResults = new ArrayList<>();
@Autowired
private QTI21Service qtiService;
@Autowired
private UserManager userManager;
public IdentitiesAssessmentItemCorrectionController(UserRequest ureq, WindowControl wControl,
AssessmentTestCorrection testCorrections, AssessmentItemRef itemRef,
RepositoryEntry testEntry, ResolvedAssessmentTest resolvedAssessmentTest) {
super(ureq, wControl, "users_interactions");
FileResourceManager frm = FileResourceManager.getInstance();
File fUnzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource());
ResourceLocator fileResourceLocator = new PathResourceLocator(fUnzippedDirRoot.toPath());
inputResourceLocator =
ImsQTI21Resource.createResolvingResourceLocator(fileResourceLocator);
assessmentObjectUri = qtiService.createAssessmentTestUri(fUnzippedDirRoot);
this.itemRef = itemRef;
this.testCorrections = testCorrections;
this.resolvedAssessmentTest = resolvedAssessmentTest;
resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful();
interactions = assessmentItem.getItemBody().findInteractions();
resourcesMapper = new ResourcesMapper(assessmentObjectUri, submissionDirectoryMaps);
mapperUri = registerCacheableMapper(null, "QTI21CorrectionsResources::" + testEntry.getKey(), resourcesMapper);
initForm(ureq);
}
protected void disableNext() {
nextQuestionButton.setEnabled(false);
}
@Override
protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
if(formLayout instanceof FormLayoutContainer) {
FormLayoutContainer layoutCont = (FormLayoutContainer)formLayout;
layoutCont.contextPut("interactionWrappers", itemResults);
List<AssessmentItemCorrection> corrections = testCorrections.getCorrections(itemRef);
if(corrections != null) {
for(AssessmentItemCorrection correction:corrections) {
initFormItemResult(correction, layoutCont);
}
}
}
FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
formLayout.add(buttonsCont);
buttonsCont.setRootForm(mainForm);
uifactory.addFormCancelButton("cancel", buttonsCont, ureq, getWindowControl());
uifactory.addFormSubmitButton("save", buttonsCont);
nextQuestionButton = uifactory.addFormLink("save.next", buttonsCont, Link.BUTTON);
}
private void initFormItemResult(AssessmentItemCorrection correction, FormLayoutContainer layoutCont) {
TestPlanNode node = correction.getItemNode();
TestPlanNodeKey testPlanNodeKey = node.getKey();
AssessmentItemSession itemSession = correction.getItemSession();
AssessmentTestSession testSession = correction.getTestSession();
TestSessionState testSessionState = correction.getTestSessionState();
boolean manualScore = false;
List<InteractionResultFormItem> responseItems = new ArrayList<>(interactions.size());
for(Interaction interaction:interactions) {
if(!(interaction instanceof EndAttemptInteraction)) {
responseItems.add(initFormExtendedTextInteraction(testPlanNodeKey, interaction, testSessionState, testSession, layoutCont));
}
if(interaction instanceof UploadInteraction
|| interaction instanceof DrawingInteraction
|| interaction instanceof ExtendedTextInteraction) {
manualScore = true;
File submissionDir = qtiService.getSubmissionDirectory(testSession);
if(submissionDir != null) {
submissionDirectoryMaps.put(testSession.getKey(), submissionDir);
}
}
}
String mScore = "";
String coachComment = "";
if(itemSession != null) {
if(itemSession.getManualScore() != null) {
mScore = AssessmentHelper.getRoundedScore(itemSession.getManualScore());
}
coachComment = itemSession.getCoachComment();
}
String fullname = userManager.getUserDisplayName(correction.getAssessedIdentity());
TextElement scoreEl = null;
if(manualScore) {
scoreEl = uifactory.addTextElement("scoreItem" + count++, "score", 6, mScore, layoutCont);
}
TextElement commentEl = uifactory.addTextAreaElement("commentItem" + count++, "comment", 2500, 4, 60, false, coachComment, layoutCont);
IdentityAssessmentItemWrapper wrapper = new IdentityAssessmentItemWrapper(fullname, assessmentItem, correction, responseItems, scoreEl, commentEl);
if(manualScore) {
Double minScore = QtiNodesExtractor.extractMinScore(assessmentItem);
Double maxScore = QtiNodesExtractor.extractMaxScore(assessmentItem);
if(maxScore != null) {
if(minScore == null) {
minScore = 0.0d;
}
wrapper.setMinScore(AssessmentHelper.getRoundedScore(minScore));
wrapper.setMaxScore(AssessmentHelper.getRoundedScore(maxScore));
wrapper.setMinScoreVal(minScore);
wrapper.setMaxScoreVal(maxScore);
scoreEl.setExampleKey("correction.min.max.score", new String[]{ wrapper.getMinScore(), wrapper.getMaxScore() });
}
}
itemResults.add(wrapper);
}
private InteractionResultFormItem initFormExtendedTextInteraction(TestPlanNodeKey testPlanNodeKey, Interaction interaction,
TestSessionState testSessionState, AssessmentTestSession assessmentTestSession, FormLayoutContainer layoutCont) {
ItemSessionState sessionState = testSessionState.getItemSessionStates().get(testPlanNodeKey);
String responseId = "responseItem" + count++;
InteractionResultFormItem responseFormItem = new InteractionResultFormItem(responseId, interaction, resolvedAssessmentItem);
responseFormItem.setItemSessionState(sessionState);
responseFormItem.setCandidateSessionContext(new TerminatedStaticCandidateSessionContext(assessmentTestSession));
responseFormItem.setResolvedAssessmentTest(resolvedAssessmentTest);
responseFormItem.setResourceLocator(inputResourceLocator);
responseFormItem.setAssessmentObjectUri(assessmentObjectUri);
responseFormItem.setMapperUri(mapperUri);
layoutCont.add(responseFormItem);
return responseFormItem;
}
@Override
protected void doDispose() {
//
}
@Override
protected boolean validateFormLogic(UserRequest ureq) {
boolean allOk = true;
if(itemResults != null) {
for(IdentityAssessmentItemWrapper itemResult:itemResults) {
allOk &= validateItemResults(itemResult);
}
}
return allOk & super.validateFormLogic(ureq);
}
private boolean validateItemResults(IdentityAssessmentItemWrapper itemResult) {
boolean allOk = true;
if(itemResult.getScoreEl() != null) {
itemResult.getScoreEl().clearError();
String scoreVal = itemResult.getScoreEl().getValue();
if(StringHelper.containsNonWhitespace(scoreVal)) {
try {
double score = Double.parseDouble(scoreVal);
boolean boundariesOk = true;
if(itemResult.getMinScore() != null && score < itemResult.getMinScoreVal().doubleValue()) {
boundariesOk &= false;
}
if(itemResult.getMaxScore() != null && score > itemResult.getMaxScoreVal().doubleValue()) {
boundariesOk &= false;
}
if(!boundariesOk) {
itemResult.getScoreEl()
.setErrorKey("correction.min.max.score", new String[]{ itemResult.getMinScore(), itemResult.getMaxScore() });
}
allOk &= boundariesOk;
} catch(Exception e) {
itemResult.getScoreEl().setErrorKey("form.error.nointeger", null);
allOk &= false;
}
}
}
return allOk;
}
@Override
protected void formOK(UserRequest ureq) {
doSave();
}
@Override
protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
if(nextQuestionButton == source) {
if(validateFormLogic(ureq)) {
doSave();
fireEvent(ureq, Event.DONE_EVENT);
}
}
super.formInnerEvent(ureq, source, event);
}
@Override
protected void formCancelled(UserRequest ureq) {
fireEvent(ureq, Event.CANCELLED_EVENT);
}
private void doSave() {
if(itemResults == null) return;
for(IdentityAssessmentItemWrapper itemResult:itemResults) {
doSave(itemResult);
}
}
private void doSave(IdentityAssessmentItemWrapper itemResult) {
AssessmentItemCorrection itemCorrection = itemResult.getCorrection();
TestSessionState testSessionState = itemCorrection.getTestSessionState();
AssessmentTestSession candidateSession = itemCorrection.getTestSession();
AssessmentSessionAuditLogger candidateAuditLogger = qtiService.getAssessmentSessionAuditLogger(candidateSession, false);
String scoreVal = null;
if(itemResult.getScoreEl() != null) {
scoreVal = itemResult.getScoreEl().getValue();
}
String commentVal = null;
if(itemResult.getCommentEl() != null) {
commentVal = itemResult.getCommentEl().getValue();
}
if(StringHelper.containsNonWhitespace(scoreVal) || commentVal != null) {
String stringuifiedIdentifier = itemResult
.getTestPlanNodeKey().getIdentifier().toString();
ParentPartItemRefs parentParts = AssessmentTestHelper
.getParentSection(itemResult.getTestPlanNodeKey(), testSessionState, resolvedAssessmentTest);
AssessmentItemSession itemSession = qtiService
.getOrCreateAssessmentItemSession(candidateSession, parentParts, stringuifiedIdentifier);
if(StringHelper.containsNonWhitespace(scoreVal)) {
BigDecimal mScore = new BigDecimal(scoreVal);
itemSession.setManualScore(mScore);
}
if(commentVal != null) {
itemSession.setCoachComment(commentVal);
}
itemSession = qtiService.updateAssessmentItemSession(itemSession);
itemCorrection.setItemSession(itemSession);
candidateAuditLogger.logCorrection(candidateSession, itemSession, getIdentity());
}
BigDecimal totalScore = null;
for(AssessmentItemCorrection corr:testCorrections.getCorrections(itemCorrection.getAssessedIdentity())) {
BigDecimal mScore = corr.getManualScore();
if(totalScore == null) {
totalScore = mScore;
} else if(mScore != null) {
totalScore = totalScore.add(mScore);
}
}
candidateSession.setManualScore(totalScore);
candidateSession = qtiService.updateAssessmentTestSession(candidateSession);
itemCorrection.setTestSession(candidateSession);
IOUtils.closeQuietly(candidateAuditLogger);
}
}