/**
* 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.
*/
package org.olat.course.run.scoring;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hibernate.LazyInitializationException;
import org.olat.core.CoreSpringFactory;
import org.olat.core.id.Identity;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.nodes.INode;
import org.olat.core.util.tree.TreeVisitor;
import org.olat.core.util.tree.Visitor;
import org.olat.course.condition.interpreter.ConditionInterpreter;
import org.olat.course.groupsandrights.CourseGroupManager;
import org.olat.course.nodes.AssessableCourseNode;
import org.olat.course.nodes.CalculatedAssessableCourseNode;
import org.olat.course.nodes.CourseNode;
import org.olat.course.nodes.PersistentAssessableCourseNode;
import org.olat.course.run.userview.UserCourseEnvironment;
import org.olat.modules.assessment.AssessmentEntry;
import org.olat.modules.assessment.model.AssessmentEntryStatus;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.RepositoryService;
import org.olat.repository.model.RepositoryEntryLifecycle;
/**
* Description:<BR/>
* The score accounting contains all score evaluations for a user
* <P/>
* Initial Date: Oct 12, 2004
*
* @author Felix Jost
*/
public class ScoreAccounting {
private static final OLog log = Tracing.createLoggerFor(ScoreAccounting.class);
private boolean error;
private final UserCourseEnvironment userCourseEnvironment;
private final Map<AssessableCourseNode, AssessmentEvaluation> cachedScoreEvals = new HashMap<>();
/**
* Constructor of the user score accounting object
* @param userCourseEnvironment
*/
public ScoreAccounting(UserCourseEnvironment userCourseEnvironment) {
this.userCourseEnvironment = userCourseEnvironment;
}
/**
* Retrieve all the score evaluations for all course nodes
*/
public void evaluateAll() {
evaluateAll(false);
}
public boolean evaluateAll(boolean update) {
Identity identity = userCourseEnvironment.getIdentityEnvironment().getIdentity();
List<AssessmentEntry> entries = userCourseEnvironment.getCourseEnvironment()
.getAssessmentManager().getAssessmentEntries(identity);
AssessableTreeVisitor visitor = new AssessableTreeVisitor(entries, update);
// collect all assessable nodes and eval 'em
CourseNode root = userCourseEnvironment.getCourseEnvironment().getRunStructure().getRootNode();
// breadth first traversal gives an easier order of evaluation for debugging
// however, for live it is absolutely mandatory to use depth first since using breadth first
// the score accoutings local cache hash map will never be used. this can slow down things like
// crazy (course with 10 tests, 300 users and some crazy score and passed calculations will have
// 10 time performance differences)
cachedScoreEvals.clear();
for(AssessmentEntry entry:entries) {
String nodeIdent = entry.getSubIdent();
CourseNode courseNode = userCourseEnvironment.getCourseEnvironment().getRunStructure().getNode(nodeIdent);
if(courseNode instanceof AssessableCourseNode) {
AssessableCourseNode acn = (AssessableCourseNode)courseNode;
AssessmentEvaluation se = AssessmentEvaluation.toAssessmentEvalutation(entry, acn);
cachedScoreEvals.put(acn, se);
}
}
TreeVisitor tv = new TreeVisitor(visitor, root, true); // true=depth first
tv.visitAll();
return visitor.hasChanges();
}
private class AssessableTreeVisitor implements Visitor {
private final boolean update;
private boolean changes = false;
private int recursionLevel = 0;
private final Map<String,AssessmentEntry> identToEntries = new HashMap<>();
public AssessableTreeVisitor(List<AssessmentEntry> entries, boolean update) {
this.update = update;
for(AssessmentEntry entry:entries) {
String ident = entry.getSubIdent();
if(identToEntries.containsKey(ident)) {
AssessmentEntry currentEntry = identToEntries.get(ident);
if(entry.getLastModified().after(currentEntry.getLastModified())) {
identToEntries.put(ident, entry);
}
} else {
identToEntries.put(ident, entry);
}
}
}
public boolean hasChanges() {
return changes;
}
@Override
public void visit(INode node) {
CourseNode cn = (CourseNode) node;
if (cn instanceof AssessableCourseNode) {
evalCourseNode((AssessableCourseNode)cn);
}
}
public AssessmentEvaluation evalCourseNode(AssessableCourseNode cn) {
// make sure we have no circular calculations
recursionLevel++;
AssessmentEvaluation se = null;
if (recursionLevel <= 15) {
se = cachedScoreEvals.get(cn);
if (se == null) { // result of this node has not been calculated yet, do it
AssessmentEntry entry = identToEntries.get(cn.getIdent());
if(cn instanceof PersistentAssessableCourseNode) {
se = ((PersistentAssessableCourseNode)cn).getUserScoreEvaluation(entry);
} else if(cn instanceof CalculatedAssessableCourseNode) {
if(update) {
se = calculateScoreEvaluation(entry, (CalculatedAssessableCourseNode)cn);
} else {
se = ((CalculatedAssessableCourseNode)cn).getUserScoreEvaluation(entry);
}
} else {
se = cn.getUserScoreEvaluation(userCourseEnvironment);
}
cachedScoreEvals.put(cn, se);
} else if(update && cn instanceof CalculatedAssessableCourseNode) {
AssessmentEntry entry = identToEntries.get(cn.getIdent());
se = calculateScoreEvaluation(entry, (CalculatedAssessableCourseNode)cn);
cachedScoreEvals.put(cn, se);
}
}
recursionLevel--;
return se;
}
/**
* Recalculate the score of structure nodes.
*
* @param entry
* @param cNode
* @return
*/
private AssessmentEvaluation calculateScoreEvaluation(AssessmentEntry entry, CalculatedAssessableCourseNode cNode) {
AssessmentEvaluation se;
if(cNode.hasScoreConfigured() || cNode.hasPassedConfigured()) {
ScoreCalculator scoreCalculator = cNode.getScoreCalculator();
String scoreExpressionStr = scoreCalculator.getScoreExpression();
String passedExpressionStr = scoreCalculator.getPassedExpression();
Float score = null;
Boolean passed = null;
Boolean userVisibility = entry == null ? null : entry.getUserVisibility();
Long assessmendId = entry == null ? null : entry.getAssessmentId();
AssessmentEntryStatus assessmentStatus = AssessmentEntryStatus.inProgress;
ConditionInterpreter ci = userCourseEnvironment.getConditionInterpreter();
if (cNode.hasScoreConfigured() && scoreExpressionStr != null) {
score = new Float(ci.evaluateCalculation(scoreExpressionStr));
}
if (cNode.hasPassedConfigured() && passedExpressionStr != null) {
boolean hasPassed = ci.evaluateCondition(passedExpressionStr);
if(hasPassed) {
passed = Boolean.TRUE;
assessmentStatus = AssessmentEntryStatus.done;
} else {
//some rules to set -> failed
FailedEvaluationType failedType = scoreCalculator.getFailedType();
if(failedType == null || failedType == FailedEvaluationType.failedAsNotPassed) {
passed = Boolean.FALSE;
} else if(failedType == FailedEvaluationType.failedAsNotPassedAfterEndDate) {
RepositoryEntryLifecycle lifecycle = getRepositoryEntryLifecycle();
if(lifecycle != null && lifecycle.getValidTo() != null && lifecycle.getValidTo().compareTo(new Date()) < 0) {
passed = Boolean.FALSE;
}
} else if(failedType == FailedEvaluationType.manual) {
passed = entry == null ? null : entry.getPassed();
}
}
}
se = new AssessmentEvaluation(score, passed, null, assessmentStatus, userVisibility, null, assessmendId, null, null);
if(entry == null) {
Identity assessedIdentity = userCourseEnvironment.getIdentityEnvironment().getIdentity();
userCourseEnvironment.getCourseEnvironment().getAssessmentManager()
.createAssessmentEntry(cNode, assessedIdentity, se);
changes = true;
} else if(!same(se, entry)) {
if(score != null) {
entry.setScore(new BigDecimal(score));
} else {
entry.setScore(null);
}
entry.setPassed(passed);
entry = userCourseEnvironment.getCourseEnvironment().getAssessmentManager().updateAssessmentEntry(entry);
identToEntries.put(cNode.getIdent(), entry);
changes = true;
}
} else {
se = AssessmentEvaluation.EMPTY_EVAL;
}
return se;
}
private RepositoryEntryLifecycle getRepositoryEntryLifecycle() {
CourseGroupManager cgm = userCourseEnvironment.getCourseEnvironment().getCourseGroupManager();
try {
RepositoryEntryLifecycle lifecycle = cgm.getCourseEntry().getLifecycle();
if(lifecycle != null) {
lifecycle.getValidTo();//
}
return lifecycle;
} catch (LazyInitializationException e) {
//OO-2667: only seen in 1 instance but as it's a critical place, secure the system
RepositoryEntry reloadedEntry = CoreSpringFactory.getImpl(RepositoryService.class)
.loadByKey(cgm.getCourseEntry().getKey());
userCourseEnvironment.getCourseEnvironment().updateCourseEntry(reloadedEntry);
return reloadedEntry.getLifecycle();
}
}
private boolean same(AssessmentEvaluation se, AssessmentEntry entry) {
boolean same = true;
if((se.getPassed() == null && entry.getPassed() != null)
|| (se.getPassed() != null && entry.getPassed() == null)
|| (se.getPassed() != null && !se.getPassed().equals(entry.getPassed()))) {
same &= false;
}
if((se.getScore() == null && entry.getScore() != null)
|| (se.getScore() != null && entry.getScore() == null)
|| (se.getScore() != null && entry.getScore() != null
&& Math.abs(se.getScore().floatValue() - entry.getScore().floatValue()) > 0.00001)) {
same &= false;
}
return same;
}
}
/**
* Get the score evaluation for a given course node without using the cache.
* @param courseNode
* @return The score evaluation
*/
public AssessmentEvaluation getScoreEvaluation(CourseNode courseNode) {
AssessmentEvaluation se = null;
if (courseNode instanceof AssessableCourseNode) {
AssessableCourseNode acn = (AssessableCourseNode) courseNode;
se = acn.getUserScoreEvaluation(userCourseEnvironment);
}
return se;
}
/**
* Evaluates the course node or simply returns the evaluation from the cache.
* @param cn
* @return ScoreEvaluation
*/
public AssessmentEvaluation evalCourseNode(AssessableCourseNode cn) {
AssessmentEvaluation se = cachedScoreEvals.get(cn);
if (se == null) { // result of this node has not been calculated yet, do it
se = cn.getUserScoreEvaluation(userCourseEnvironment);
cachedScoreEvals.put(cn, se);
}
return se;
}
/**
* Evaluate the score of the course element. The method
* takes the visibility of the results in account and will
* return 0.0 if the results are not visiblity.
*
* @param childId The specified course element ident
* @return A float (never null)
*/
public Float evalScoreOfCourseNode(String childId) {
CourseNode foundNode = findChildByID(childId);
Float score = null;
if (foundNode instanceof AssessableCourseNode) {
AssessableCourseNode acn = (AssessableCourseNode) foundNode;
ScoreEvaluation se = evalCourseNode(acn);
if(se != null) {
// the node could not provide any sensible information on scoring. e.g. a STNode with no calculating rules
if(se.getUserVisible() == null || se.getUserVisible().booleanValue()) {
score = se.getScore();
} else {
score = new Float(0.0f);
}
}
if (score == null) { // a child has no score yet
score = new Float(0.0f); // default to 0.0, so that the condition can be evaluated (zero points makes also the most sense for "no results yet", if to be expressed in a number)
}
} else {
error = true;
score = new Float(0.0f);
}
return score;
}
/**
* Evaluate the passed / failed state of a course element. The method
* takes the visibility of the results in account and will return false
* if the results are not visible.
*
* @param childId The specified course element ident
* @return true/false never null
*/
public Boolean evalPassedOfCourseNode(String childId) {
CourseNode foundNode = findChildByID(childId);
if (foundNode == null) {
error = true;
return Boolean.FALSE;
}
if (!(foundNode instanceof AssessableCourseNode)) {
error = true;
return Boolean.FALSE;
}
AssessableCourseNode acn = (AssessableCourseNode) foundNode;
ScoreEvaluation se = evalCourseNode(acn);
if (se == null) { // the node could not provide any sensible information on scoring. e.g. a STNode with no calculating rules
log.error("could not evaluate node '" + acn.getShortTitle() + "' (" + acn.getClass().getName() + "," + childId + ")", null);
return Boolean.FALSE;
}
// check if the results are visible
if(se.getUserVisible() != null && !se.getUserVisible().booleanValue()) {
return Boolean.FALSE;
}
Boolean passed = se.getPassed();
if (passed == null) { // a child has no "Passed" yet
passed = Boolean.FALSE;
}
return passed;
}
private CourseNode findChildByID(String id) {
return userCourseEnvironment.getCourseEnvironment().getRunStructure().getNode(id);
}
/**
* @return true if an error occured
*/
public boolean isError() {
return error;
}
}