/**
* 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.
* <p>
*/
package org.olat.course.assessment;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.olat.core.id.Identity;
import org.olat.core.id.IdentityEnvironment;
import org.olat.core.logging.Tracing;
import org.olat.core.util.Formatter;
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.ICourse;
import org.olat.course.nodes.AssessableCourseNode;
import org.olat.course.nodes.CourseNode;
import org.olat.course.nodes.ProjectBrokerCourseNode;
import org.olat.course.nodes.STCourseNode;
import org.olat.course.nodes.ScormCourseNode;
import org.olat.course.nodes.iq.IQEditController;
import org.olat.course.run.scoring.ScoreEvaluation;
import org.olat.course.run.userview.UserCourseEnvironment;
import org.olat.course.run.userview.UserCourseEnvironmentImpl;
import org.olat.course.tree.CourseEditorTreeModel;
import org.olat.course.tree.CourseEditorTreeNode;
import org.olat.modules.ModuleConfiguration;
/**
* Description:<br>
* Helper methods for the course assessment system
* <P>
* Initial Date: Oct 28, 2004<br>
* @author gnaegi
*/
public class AssessmentHelper {
/**
* String to symbolize 'not available' or 'not assigned' in assessments
* details *
*/
public static final String DETAILS_NA_VALUE = "n/a";
/** Highes score value supported by OLAT * */
public static final float MAX_SCORE_SUPPORTED = 10000f;
/** Lowest score value supported by OLAT * */
public static final float MIN_SCORE_SUPPORTED = -10000f;
/**
* Wraps an identity and it's score evaluation / attempts in a wrapper object
* for a given course node
*
* @param identity
* @param localUserCourseEnvironmentCache
* @param course the course
* @param courseNode an assessable course node or null if no details and
* attempts must be fetched
* @return a wrapped identity
*/
static AssessedIdentityWrapper wrapIdentity(Identity identity, Map<Long,UserCourseEnvironment> localUserCourseEnvironmentCache, ICourse course,
AssessableCourseNode courseNode) {
// Try to get user course environment from local hash map cache. If not
// successful
// create the environment and add it to the map for later performance
// optimization
//synchronized (localUserCourseEnvironmentCache) { //o_clusterOK by:ld - no need to synchronized - only local variables
UserCourseEnvironment uce = localUserCourseEnvironmentCache.get(identity.getKey());
if (uce == null) {
uce = createAndInitUserCourseEnvironment(identity, course);
// add to cache for later usage
localUserCourseEnvironmentCache.put(identity.getKey(), uce);
if (Tracing.isDebugEnabled(AssessmentHelper.class)){
Tracing.logDebug("localUserCourseEnvironmentCache hit failed, adding course environment for user::"
+ identity.getName(), AssessmentHelper.class);
}
}
return wrapIdentity(uce, courseNode);
//}
}
/**
* Wraps an identity and it's score evaluation / attempts in a wrapper object
* for a given course node
*
* @param uce The users course environment. Must be initialized
* (uce.getScoreAccounting().evaluateAll() must be called previously)
* @param courseNode an assessable course node or null if no details and
* attempts must be fetched
* @return a wrapped identity
*/
static AssessedIdentityWrapper wrapIdentity(UserCourseEnvironment uce, AssessableCourseNode courseNode) {
// Fetch attempts and details for this node if available
Integer attempts = null;
String details = null;
if (courseNode != null) {
if (courseNode.hasAttemptsConfigured()) {
attempts = courseNode.getUserAttempts(uce);
}
if (courseNode.hasDetails()) {
details = courseNode.getDetailsListView(uce);
if (details == null) details = DETAILS_NA_VALUE;
}
}
AssessedIdentityWrapper aiw = new AssessedIdentityWrapper(uce, attempts, details);
return aiw;
}
/**
* Create a user course environment for the given user and course. After
* creation, the users score accounting will be initialized.
*
* @param identity
* @param course
* @return Initialized user course environment
*/
static UserCourseEnvironment createAndInitUserCourseEnvironment(Identity identity, ICourse course) {
// create an identenv with no roles, no attributes, no locale
IdentityEnvironment ienv = new IdentityEnvironment();
ienv.setIdentity(identity);
UserCourseEnvironment uce = new UserCourseEnvironmentImpl(ienv, course.getCourseEnvironment());
// Fetch all score and passed and calculate score accounting for the entire
// course
uce.getScoreAccounting().evaluateAll();
return uce;
}
/**
* check the given node for assessability.
* @param node
* @return
*/
public static boolean checkIfNodeIsAssessable(CourseNode node) {
if (node instanceof AssessableCourseNode) {
if (node instanceof STCourseNode) {
STCourseNode scn = (STCourseNode) node;
if (scn.hasPassedConfigured() || scn.hasScoreConfigured()) { return true; }
} else if (node instanceof ScormCourseNode) {
ScormCourseNode scormn = (ScormCourseNode) node;
if (scormn.hasScoreConfigured()) { return true; }
} else if (node instanceof ProjectBrokerCourseNode) {
return false;// TODO:cg 28.01.2010 ProjectBroker : no assessment-tool in V1.0 return always false
} else {
return true;
}
}
return false;
}
/**
* Checks recursivley a course structure or a part of it for assessable nodes
* or for structure course nodes (subtype of assessable node), which
* 'hasPassedConfigured' or 'hasScoreConfigured' is true. If founds the first
* node that meets the criterias, it returns true.
*
* @param node
* @return boolean
*/
public static boolean checkForAssessableNodes(CourseNode node) {
if(checkIfNodeIsAssessable(node)) {
return true;
}
// check children now
int count = node.getChildCount();
for (int i = 0; i < count; i++) {
CourseNode cn = (CourseNode) node.getChildAt(i);
if (checkForAssessableNodes(cn)) return true;
}
return false;
}
/**
* Get all assessable nodes including the root node (if assessable)
*
* @param editorModel
* @param excludeNode Node that should be excluded in the list, e.g. the
* current node or null if all assessable nodes should be used
* @return List of assessable course nodes
*/
public static List<CourseNode> getAssessableNodes(final CourseEditorTreeModel editorModel, final CourseNode excludeNode) {
CourseEditorTreeNode rootNode = (CourseEditorTreeNode) editorModel.getRootNode();
final List<CourseNode> nodes = new ArrayList<CourseNode>();
// visitor class: takes all assessable nodes if not the exclude node and
// puts
// them into the nodes list
Visitor visitor = new Visitor() {
public void visit(INode node) {
CourseEditorTreeNode editorNode = (CourseEditorTreeNode) node;
CourseNode courseNode = editorModel.getCourseNode(node.getIdent());
if (!editorNode.isDeleted() && (courseNode != excludeNode)) {
if(checkIfNodeIsAssessable(courseNode)) {
nodes.add(courseNode);
}
}
}
};
// not visit beginning at the root node
TreeVisitor tv = new TreeVisitor(visitor, rootNode, false);
tv.visitAll();
return nodes;
}
/**
* @param score The score to be rounded
* @return The rounded score for GUI presentation
*/
public static String getRoundedScore(Float score) {
if (score == null) return null;
return Formatter.roundToString(score.floatValue(), 3);
}
public static final String KEY_TYPE = "type";
public static final String KEY_IDENTIFYER = "identifyer";
public static final String KEY_INDENT = "indent";
public static final String KEY_TITLE_SHORT = "short.title";
public static final String KEY_TITLE_LONG = "long.title";
public static final String KEY_PASSED = "passed";
public static final String KEY_SCORE = "score";
public static final String KEY_ATTEMPTS = "attempts";
public static final String KEY_DETAILS = "details";
public static final String KEY_SELECTABLE = "selectable";
/**
* Add all assessable nodes and the scoring data to a list. Each item in the list is an object array
* that has the following data:
* @param recursionLevel
* @param courseNode
* @param userCourseEnv
* @param discardEmptyNodes
* @param discardComments
* @return list of object arrays or null if empty
*/
static List<Map<String,Object>> addAssessableNodeAndDataToList(int recursionLevel, CourseNode courseNode, UserCourseEnvironment userCourseEnv, boolean discardEmptyNodes, boolean discardComments) {
// 1) Get list of children data using recursion of this method
List<Map<String, Object>> childrenData = new ArrayList<Map<String, Object>>(50);
for (int i = 0; i < courseNode.getChildCount(); i++) {
CourseNode child = (CourseNode) courseNode.getChildAt(i);
List<Map<String, Object>> childData = addAssessableNodeAndDataToList( (recursionLevel + 1), child, userCourseEnv, discardEmptyNodes, discardComments);
if (childData != null)
childrenData.addAll(childData);
}
// 2) Get data of this node only if
// - it has any wrapped children or
// - it is of an assessable course node type
boolean hasDisplayableValuesConfigured = false;
boolean hasDisplayableUserValues = false;
if (childrenData.size() > 0 || courseNode instanceof AssessableCourseNode) {
// Store node and user data in object array. This object array serves as data model for
// the user assessment overview table
Map<String,Object> nodeData = new HashMap<String, Object>();
// indent
nodeData.put(KEY_INDENT, new Integer(recursionLevel));
// course node data
nodeData.put(KEY_TYPE, courseNode.getType());
nodeData.put(KEY_TITLE_SHORT, courseNode.getShortTitle());
nodeData.put(KEY_TITLE_LONG, courseNode.getLongTitle());
nodeData.put(KEY_IDENTIFYER, courseNode.getIdent());
if (courseNode instanceof AssessableCourseNode) {
AssessableCourseNode assessableCourseNode = (AssessableCourseNode) courseNode;
ScoreEvaluation scoreEvaluation = userCourseEnv.getScoreAccounting().getScoreEvaluation(courseNode);
// details
if (assessableCourseNode.hasDetails()) {
hasDisplayableValuesConfigured = true;
String detailValue = assessableCourseNode.getDetailsListView(userCourseEnv);
if (detailValue == null) {
// ignore unset details in discardEmptyNodes mode
nodeData.put(KEY_DETAILS, AssessmentHelper.DETAILS_NA_VALUE);
} else {
nodeData.put(KEY_DETAILS, detailValue);
hasDisplayableUserValues = true;
}
}
// attempts
if (assessableCourseNode.hasAttemptsConfigured()) {
hasDisplayableValuesConfigured = true;
Integer attemptsValue = assessableCourseNode.getUserAttempts(userCourseEnv);
if (attemptsValue != null) {
nodeData.put(KEY_ATTEMPTS, attemptsValue);
if (attemptsValue.intValue() > 0) {
// ignore attempts = 0 in discardEmptyNodes mode
hasDisplayableUserValues = true;
}
}
}
// score
if (assessableCourseNode.hasScoreConfigured()) {
hasDisplayableValuesConfigured = true;
Float score = scoreEvaluation.getScore();
if (score != null) {
nodeData.put(KEY_SCORE, AssessmentHelper.getRoundedScore(score));
hasDisplayableUserValues = true;
}
}
// passed
if (assessableCourseNode.hasPassedConfigured()) {
hasDisplayableValuesConfigured = true;
Boolean passed = scoreEvaluation.getPassed();
if (passed != null) {
nodeData.put(KEY_PASSED, passed);
hasDisplayableUserValues = true;
}
}
// selection command available
AssessableCourseNode acn = (AssessableCourseNode) courseNode;
if (acn.isEditableConfigured()) {
// Assessable course nodes are selectable
nodeData.put(KEY_SELECTABLE, Boolean.TRUE);
} else {
// assessable nodes that do not have score or passed are not selectable
// (e.g. a st node with no defined rule
nodeData.put(KEY_SELECTABLE, Boolean.FALSE);
}
if (!hasDisplayableUserValues && assessableCourseNode.hasCommentConfigured() && !discardComments) {
// comments are invisible in the table but if configured the node must be in the list
// for the efficiency statement this can be ignored, this is the case when discardComments is true
hasDisplayableValuesConfigured = true;
if (assessableCourseNode.getUserUserComment(userCourseEnv) != null) {
hasDisplayableUserValues = true;
}
}
} else {
// Not assessable nodes are not selectable. (e.g. a node that
// has an assessable child node but is itself not assessable)
nodeData.put(KEY_SELECTABLE, Boolean.FALSE);
}
// 3) Add data of this node to mast list if node assessable or children list has any data.
// Do only add nodes when they have any assessable element, otherwhise discard (e.g. empty course,
// structure nodes without scoring rules)! When the discardEmptyNodes flag is set then only
// add this node when there is user data found for this node.
if (childrenData.size() > 0
|| (discardEmptyNodes && hasDisplayableValuesConfigured && hasDisplayableUserValues)
|| (!discardEmptyNodes && hasDisplayableValuesConfigured)) {
List<Map<String, Object>> nodeAndChildren = new ArrayList<Map<String, Object>>();
nodeAndChildren.add(nodeData);
// 4) Add children data list to master list
nodeAndChildren.addAll(childrenData);
return nodeAndChildren;
}
}
return null;
}
/**
* Evaluates if the results are visble or not in respect of the configured CONFIG_KEY_DATE_DEPENDENT_RESULTS parameter. <br>
* The results are always visible if no date dependent,
* or if date dependent only in the period: startDate-endDate.
* EndDate could be null, that is there is no restriction for the end date.
*
* @return true if is visible.
*/
public static boolean isResultVisible(ModuleConfiguration modConfig) {
boolean isVisible = false;
Boolean showResultsActive = (Boolean)modConfig.get(IQEditController.CONFIG_KEY_DATE_DEPENDENT_RESULTS);
if(showResultsActive!=null && showResultsActive.booleanValue()) {
Date startDate = (Date)modConfig.get(IQEditController.CONFIG_KEY_RESULTS_START_DATE);
Date endDate = (Date)modConfig.get(IQEditController.CONFIG_KEY_RESULTS_END_DATE);
Date currentDate = new Date();
if(currentDate.after(startDate) && (endDate==null || currentDate.before(endDate))) {
isVisible = true;
}
} else {
isVisible = true;
}
return isVisible;
}
}