From dce48a1d39b0e18a2557e5f123028857dc026d2d Mon Sep 17 00:00:00 2001 From: srosse <none@none> Date: Tue, 30 Aug 2016 11:16:43 +0200 Subject: [PATCH] OO-1593: log some action start / close / edit around the QTI 2.1 editor and runtime --- .../activity/OlatResourceableType.java | 3 + .../iq/QTI21AssessmentRunController.java | 21 +++- .../olat/ims/qti21/QTI21LoggingAction.java | 114 ++++++++++++++++++ .../ui/AssessmentEntryOutcomesListener.java | 22 +++- .../ui/AssessmentTestDisplayController.java | 15 ++- .../AssessmentTestComposerController.java | 7 ++ .../MultipleChoiceEditorController.java | 10 +- .../SingleChoiceEditorController.java | 10 +- .../logging/activity/LoggingResourceable.java | 5 + 9 files changed, 191 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/olat/ims/qti21/QTI21LoggingAction.java diff --git a/src/main/java/org/olat/core/logging/activity/OlatResourceableType.java b/src/main/java/org/olat/core/logging/activity/OlatResourceableType.java index 91d9955157f..6d9e11f8177 100644 --- a/src/main/java/org/olat/core/logging/activity/OlatResourceableType.java +++ b/src/main/java/org/olat/core/logging/activity/OlatResourceableType.java @@ -65,6 +65,9 @@ public enum OlatResourceableType implements ILoggingResourceableType { /** represents a content package **/ cp, + /** this represents a QTi test **/ + test, + /** represents a shared folder **/ sharedFolder, diff --git a/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java b/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java index 2c72de65cfc..b32f18e9b70 100644 --- a/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java +++ b/src/main/java/org/olat/course/nodes/iq/QTI21AssessmentRunController.java @@ -22,6 +22,7 @@ package org.olat.course.nodes.iq; import java.text.DateFormat; import java.util.Date; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.olat.core.commons.fullWebApp.LayoutMain3ColsController; import org.olat.core.gui.UserRequest; @@ -57,6 +58,7 @@ import org.olat.ims.qti.process.AssessmentInstance; import org.olat.ims.qti21.OutcomesListener; import org.olat.ims.qti21.QTI21DeliveryOptions; import org.olat.ims.qti21.QTI21DeliveryOptions.ShowResultsOnFinish; +import org.olat.ims.qti21.QTI21LoggingAction; import org.olat.ims.qti21.QTI21Service; import org.olat.ims.qti21.ui.AssessmentTestDisplayController; import org.olat.ims.qti21.ui.QTI21Event; @@ -97,6 +99,7 @@ public class QTI21AssessmentRunController extends BasicController implements Gen private AssessmentTestDisplayController displayCtrl; private LayoutMain3ColsController displayContainerController; + private AtomicBoolean incrementAttempts = new AtomicBoolean(true); @Autowired private QTI21Service qtiService; @@ -115,6 +118,8 @@ public class QTI21AssessmentRunController extends BasicController implements Gen singleUserEventCenter = userSession.getSingleUserEventCenter(); mainVC = createVelocityContainer("assessment_run"); + addLoggingResourceable(LoggingResourceable.wrap(courseNode)); + if(courseNode instanceof IQTESTCourseNode) { mainVC.contextPut("type", "test"); } else if(courseNode instanceof IQSELFCourseNode) { @@ -321,7 +326,9 @@ public class QTI21AssessmentRunController extends BasicController implements Gen assessmentStopped = false; singleUserEventCenter.registerFor(this, getIdentity(), assessmentInstanceOres); - singleUserEventCenter.fireEventToListenersOf(new AssessmentEvent(AssessmentEvent.TYPE.STARTED, ureq.getUserSession()), assessmentEventOres); + singleUserEventCenter.fireEventToListenersOf(new AssessmentEvent(AssessmentEvent.TYPE.STARTED, ureq.getUserSession()), assessmentEventOres); + + ThreadLocalUserActivityLogger.log(QTI21LoggingAction.QTI_START_IN_COURSE, getClass()); } } @@ -385,9 +392,17 @@ public class QTI21AssessmentRunController extends BasicController implements Gen assessmentStatus = AssessmentEntryStatus.done; } ScoreEvaluation sceval = new ScoreEvaluation(score, pass, assessmentStatus, Boolean.TRUE, assessmentId); - ((IQTESTCourseNode)courseNode).updateUserScoreEvaluation(sceval, userCourseEnv, getIdentity(), true); + + boolean increment = incrementAttempts.getAndSet(false); + ((IQTESTCourseNode)courseNode).updateUserScoreEvaluation(sceval, userCourseEnv, getIdentity(), increment); + if(increment) { + ThreadLocalUserActivityLogger.log(QTI21LoggingAction.QTI_CLOSE_IN_COURSE, getClass()); + } } else if(courseNode instanceof IQSELFCourseNode) { - ((IQSELFCourseNode)courseNode).incrementUserAttempts(userCourseEnv); + boolean increment = incrementAttempts.getAndSet(false); + if(increment) { + ((IQSELFCourseNode)courseNode).incrementUserAttempts(userCourseEnv); + } } } } diff --git a/src/main/java/org/olat/ims/qti21/QTI21LoggingAction.java b/src/main/java/org/olat/ims/qti21/QTI21LoggingAction.java new file mode 100644 index 00000000000..6172525463d --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/QTI21LoggingAction.java @@ -0,0 +1,114 @@ +/** +* OLAT - Online Learning and Training<br> +* http://www.olat.org +* <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 +* <p> +* http://www.apache.org/licenses/LICENSE-2.0 +* <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> +* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> +* University of Zurich, Switzerland. +* <hr> +* <a href="http://www.openolat.org"> +* OpenOLAT - Online Learning and Training</a><br> +* This file has been modified by the OpenOLAT community. Changes are licensed +* under the Apache 2.0 license as the original file. +* <p> +*/ + +package org.olat.ims.qti21; + +import java.lang.reflect.Field; + +import org.olat.core.logging.activity.ActionObject; +import org.olat.core.logging.activity.ActionType; +import org.olat.core.logging.activity.ActionVerb; +import org.olat.core.logging.activity.BaseLoggingAction; +import org.olat.core.logging.activity.CrudAction; +import org.olat.core.logging.activity.ILoggingAction; +import org.olat.core.logging.activity.OlatResourceableType; +import org.olat.core.logging.activity.ResourceableTypeList; + +/** + * + * Initial date: 30.08.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QTI21LoggingAction extends BaseLoggingAction { + + // the following is a user clicking within a test + public static final ILoggingAction QTI_START_IN_COURSE = + new QTI21LoggingAction(ActionType.statistic, CrudAction.update, ActionVerb.launch, ActionObject.test).setTypeList( + new ResourceableTypeList(). + addMandatory(OlatResourceableType.course, OlatResourceableType.node) + ); + + public static final ILoggingAction QTI_CLOSE_IN_COURSE = + new QTI21LoggingAction(ActionType.statistic, CrudAction.update, ActionVerb.close , ActionObject.test).setTypeList( + new ResourceableTypeList(). + addMandatory(OlatResourceableType.course, OlatResourceableType.node) + ); + + public static final ILoggingAction QTI_START_AS_RESOURCE = + new QTI21LoggingAction(ActionType.statistic, CrudAction.update, ActionVerb.launch, ActionObject.test).setTypeList( + new ResourceableTypeList(). + addMandatory(OlatResourceableType.test) + ); + + public static final ILoggingAction QTI_CLOSE_AS_RESOURCE = + new QTI21LoggingAction(ActionType.statistic, CrudAction.update, ActionVerb.close , ActionObject.test).setTypeList( + new ResourceableTypeList(). + addMandatory(OlatResourceableType.test) + ); + + public static final ILoggingAction QTI_EDIT_RESOURCE = + new QTI21LoggingAction(ActionType.statistic, CrudAction.update, ActionVerb.edit , ActionObject.test).setTypeList( + new ResourceableTypeList(). + addMandatory(OlatResourceableType.test) + ); + + + /** + * This static constructor's only use is to set the javaFieldIdForDebug + * on all of the LoggingActions defined in this class. + * <p> + * This is used to simplify debugging - as it allows to issue (technical) log + * statements where the name of the LoggingAction Field is written. + */ + static { + Field[] fields = QTI21LoggingAction.class.getDeclaredFields(); + if (fields!=null) { + for (int i = 0; i < fields.length; i++) { + Field field = fields[i]; + if (field.getType()==QTI21LoggingAction.class) { + try { + QTI21LoggingAction aLoggingAction = (QTI21LoggingAction)field.get(null); + aLoggingAction.setJavaFieldIdForDebug(field.getName()); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + } + } + + /** + * Simple wrapper calling super<init> + * @see BaseLoggingAction#BaseLoggingAction(ActionType, CrudAction, ActionVerb, String) + */ + QTI21LoggingAction(ActionType resourceActionType, CrudAction action, ActionVerb actionVerb, ActionObject actionObject) { + super(resourceActionType, action, actionVerb, actionObject.name()); + } + +} diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentEntryOutcomesListener.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentEntryOutcomesListener.java index c5cde952f00..9a5d76044d1 100644 --- a/src/main/java/org/olat/ims/qti21/ui/AssessmentEntryOutcomesListener.java +++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentEntryOutcomesListener.java @@ -20,8 +20,11 @@ package org.olat.ims.qti21.ui; import java.math.BigDecimal; +import java.util.concurrent.atomic.AtomicBoolean; +import org.olat.core.logging.activity.ThreadLocalUserActivityLogger; import org.olat.ims.qti21.OutcomesListener; +import org.olat.ims.qti21.QTI21LoggingAction; import org.olat.modules.assessment.AssessmentEntry; import org.olat.modules.assessment.AssessmentService; import org.olat.modules.assessment.model.AssessmentEntryStatus; @@ -36,11 +39,18 @@ public class AssessmentEntryOutcomesListener implements OutcomesListener { private AssessmentEntry assessmentEntry; private final AssessmentService assessmentService; + + private final boolean authorMode; private final boolean needManualCorrection; + + private AtomicBoolean start = new AtomicBoolean(true); + private AtomicBoolean close = new AtomicBoolean(true); - public AssessmentEntryOutcomesListener(AssessmentEntry assessmentEntry, boolean needManualCorrection, AssessmentService assessmentService) { + public AssessmentEntryOutcomesListener(AssessmentEntry assessmentEntry, boolean needManualCorrection, + AssessmentService assessmentService, boolean authorMode) { this.assessmentEntry = assessmentEntry; this.assessmentService = assessmentService; + this.authorMode = authorMode; this.needManualCorrection = needManualCorrection; } @@ -49,6 +59,11 @@ public class AssessmentEntryOutcomesListener implements OutcomesListener { AssessmentEntryStatus assessmentStatus = AssessmentEntryStatus.inProgress; assessmentEntry.setAssessmentStatus(assessmentStatus); assessmentEntry = assessmentService.updateAssessmentEntry(assessmentEntry); + + boolean firstStart = start.getAndSet(false); + if(firstStart && !authorMode) { + ThreadLocalUserActivityLogger.log(QTI21LoggingAction.QTI_START_AS_RESOURCE, getClass()); + } } @Override @@ -68,5 +83,10 @@ public class AssessmentEntryOutcomesListener implements OutcomesListener { assessmentEntry.setPassed(submittedPass); assessmentEntry.setAssessmentId(assessmentId); assessmentEntry = assessmentService.updateAssessmentEntry(assessmentEntry); + + boolean firstClose = close.getAndSet(false); + if(firstClose && !authorMode) { + ThreadLocalUserActivityLogger.log(QTI21LoggingAction.QTI_CLOSE_AS_RESOURCE, getClass()); + } } } diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java index 1c372f7a7bf..7aa9fb28462 100644 --- a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java +++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java @@ -91,6 +91,7 @@ import org.olat.ims.qti21.ui.event.RetrieveAssessmentTestSessionEvent; import org.olat.modules.assessment.AssessmentEntry; import org.olat.modules.assessment.AssessmentService; import org.olat.repository.RepositoryEntry; +import org.olat.util.logging.activity.LoggingResourceable; import org.springframework.beans.factory.annotation.Autowired; import uk.ac.ed.ph.jqtiplus.JqtiPlus; @@ -218,6 +219,12 @@ public class AssessmentTestDisplayController extends BasicController implements anonymousIdentifier = null; } + if(testEntry == entry) { + // Limit to the case where the test is launched as resource, + // within course is this task delegated to the QTI21AssessmentRunController + addLoggingResourceable(LoggingResourceable.wrapTest(entry)); + } + FileResourceManager frm = FileResourceManager.getInstance(); fUnzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource()); resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(fUnzippedDirRoot, false, false); @@ -272,7 +279,7 @@ public class AssessmentTestDisplayController extends BasicController implements AssessmentEntry assessmentEntry = assessmentService.getOrCreateAssessmentEntry(assessedIdentity, anonymousIdentifier, entry, subIdent, testEntry); if(outcomesListener == null) { boolean manualCorrections = AssessmentTestHelper.needManualCorrection(resolvedAssessmentTest); - outcomesListener = new AssessmentEntryOutcomesListener(assessmentEntry, manualCorrections, assessmentService); + outcomesListener = new AssessmentEntryOutcomesListener(assessmentEntry, manualCorrections, assessmentService, authorMode); } AssessmentTestSession lastSession = null; @@ -877,11 +884,11 @@ public class AssessmentTestDisplayController extends BasicController implements testSessionController.endCurrentTestPart(requestTimestamp); TestSessionState testSessionState = testSessionController.getTestSessionState(); + TestPlanNode nextTestPart = testSessionController.findNextEnterableTestPart(); // Record current result state - final AssessmentResult assessmentResult = computeAndRecordTestAssessmentResult(ureq, testSessionState, false); - - if(testSessionController.findNextEnterableTestPart() == null) { + final AssessmentResult assessmentResult = computeAndRecordTestAssessmentResult(ureq, testSessionState, nextTestPart == null); + if(nextTestPart == null) { candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult, requestTimestamp); } } diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java index 26375917801..6f098a9da46 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java @@ -62,6 +62,7 @@ import org.olat.core.gui.control.generic.wizard.StepsMainRunController; import org.olat.core.gui.control.generic.wizard.StepsRunContext; import org.olat.core.gui.media.MediaResource; import org.olat.core.helpers.Settings; +import org.olat.core.logging.activity.ThreadLocalUserActivityLogger; import org.olat.core.util.Formatter; import org.olat.core.util.Util; import org.olat.core.util.coordinate.CoordinatorManager; @@ -69,6 +70,7 @@ import org.olat.core.util.coordinate.LockResult; import org.olat.core.util.vfs.VFSContainer; import org.olat.fileresource.FileResourceManager; import org.olat.ims.qti21.QTI21Constants; +import org.olat.ims.qti21.QTI21LoggingAction; import org.olat.ims.qti21.QTI21Service; import org.olat.ims.qti21.manager.openxml.QTI21WordExport; import org.olat.ims.qti21.model.IdentifierGenerator; @@ -103,6 +105,7 @@ import org.olat.modules.qpool.ui.events.QItemViewEvent; import org.olat.repository.RepositoryEntry; import org.olat.repository.ui.RepositoryEntryRuntimeController.ToolbarAware; import org.olat.user.UserManager; +import org.olat.util.logging.activity.LoggingResourceable; import org.springframework.beans.factory.annotation.Autowired; import uk.ac.ed.ph.jqtiplus.node.QtiNode; @@ -195,6 +198,8 @@ public class AssessmentTestComposerController extends MainLayoutBasicController return; } + addLoggingResourceable(LoggingResourceable.wrapTest(testEntry)); + // test structure menuTree = new MenuTree("atTree"); menuTree.setExpandSelectedNode(false); @@ -866,6 +871,8 @@ public class AssessmentTestComposerController extends MainLayoutBasicController URI testURI = resolvedAssessmentTest.getTestLookup().getSystemId(); File testFile = new File(testURI); qtiService.updateAssesmentObject(testFile, resolvedAssessmentTest); + + ThreadLocalUserActivityLogger.log(QTI21LoggingAction.QTI_EDIT_RESOURCE, getClass()); } private void recalculateMaxScoreAssessmentTest() { diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MultipleChoiceEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MultipleChoiceEditorController.java index f752b3c6581..2475feede0a 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MultipleChoiceEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MultipleChoiceEditorController.java @@ -218,10 +218,12 @@ public class MultipleChoiceEditorController extends FormBasicController { } answersCont.clearError(); - String[] correctAnswers = ureq.getHttpReq().getParameterValues("correct"); - if(correctAnswers == null || correctAnswers.length == 0) { - answersCont.setErrorKey("error.need.correct.answer", null); - allOk &= false; + if(!restrictedEdit) { + String[] correctAnswers = ureq.getHttpReq().getParameterValues("correct"); + if(correctAnswers == null || correctAnswers.length == 0) { + answersCont.setErrorKey("error.need.correct.answer", null); + allOk &= false; + } } return allOk & super.validateFormLogic(ureq); diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java index 998eca4e51d..82a0d3d55d1 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java @@ -214,10 +214,12 @@ public class SingleChoiceEditorController extends FormBasicController { } answersCont.clearError(); - String correctAnswer = ureq.getParameter("correct"); - if(!StringHelper.containsNonWhitespace(correctAnswer)) { - allOk &= false; - answersCont.setErrorKey("error.need.correct.answer", null); + if(!restrictedEdit) { + String correctAnswer = ureq.getParameter("correct"); + if(!StringHelper.containsNonWhitespace(correctAnswer)) { + allOk &= false; + answersCont.setErrorKey("error.need.correct.answer", null); + } } return allOk & super.validateFormLogic(ureq); diff --git a/src/main/java/org/olat/util/logging/activity/LoggingResourceable.java b/src/main/java/org/olat/util/logging/activity/LoggingResourceable.java index 2b047a83a55..2c2558a55ef 100644 --- a/src/main/java/org/olat/util/logging/activity/LoggingResourceable.java +++ b/src/main/java/org/olat/util/logging/activity/LoggingResourceable.java @@ -456,6 +456,11 @@ public class LoggingResourceable implements ILoggingResourceable { String.valueOf(course.getResourceableId()), course.getCourseTitle(), false); } + public static LoggingResourceable wrapTest(RepositoryEntry entry) { + return new LoggingResourceable(entry, OlatResourceableType.test, entry.getOlatResource().getResourceableTypeName(), + String.valueOf(entry.getOlatResource().getResourceableId()), entry.getDisplayname(), false); + } + /** * Wraps a CourseNode into a LoggingResourceable - setting type/id/name accordingly * @param node the node to be wrapped -- GitLab