org.sakaiproject.tool.assessment.services.GradingService Maven / Gradle / Ivy
/**********************************************************************************
* $URL$
* $Id$
***********************************************************************************
*
* Copyright (c) 2004, 2005, 2006, 2007, 2008, 2009 The Sakai Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
**********************************************************************************/
package org.sakaiproject.tool.assessment.services;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.time.Instant;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.complex.Complex;
import org.apache.commons.math3.complex.ComplexFormat;
import org.apache.commons.math3.exception.MathParseException;
import org.apache.commons.math3.util.Precision;
import org.mariuszgromada.math.mxparser.parsertokens.ParserSymbol;
import org.sakaiproject.event.cover.EventTrackingService;
import org.sakaiproject.samigo.util.SamigoConstants;
import org.sakaiproject.spring.SpringBeanLocator;
import org.sakaiproject.tool.assessment.data.dao.assessment.EventLogData;
import org.sakaiproject.tool.assessment.data.dao.assessment.PublishedItemData;
import org.sakaiproject.tool.assessment.data.dao.grading.AssessmentGradingAttachment;
import org.sakaiproject.tool.assessment.data.dao.grading.AssessmentGradingData;
import org.sakaiproject.tool.assessment.data.dao.grading.ItemGradingAttachment;
import org.sakaiproject.tool.assessment.data.dao.grading.ItemGradingData;
import org.sakaiproject.tool.assessment.data.dao.grading.MediaData;
import org.sakaiproject.tool.assessment.data.dao.grading.StudentGradingSummaryData;
import org.sakaiproject.tool.assessment.data.ifc.assessment.AnswerIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.AssessmentAccessControlIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.AssessmentIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.EvaluationModelIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemDataIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemMetaDataIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemTextIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.PublishedAssessmentIfc;
import org.sakaiproject.tool.assessment.data.ifc.grading.StudentGradingSummaryIfc;
import org.sakaiproject.tool.assessment.data.ifc.shared.TypeIfc;
import org.sakaiproject.tool.assessment.facade.AgentFacade;
import org.sakaiproject.tool.assessment.facade.EventLogFacade;
import org.sakaiproject.tool.assessment.facade.GradebookFacade;
import org.sakaiproject.tool.assessment.facade.PublishedAssessmentFacade;
import org.sakaiproject.tool.assessment.facade.TypeFacade;
import org.sakaiproject.tool.assessment.facade.TypeFacadeQueriesAPI;
import org.sakaiproject.tool.assessment.integration.context.IntegrationContextFactory;
import org.sakaiproject.tool.assessment.integration.helper.ifc.GradebookServiceHelper;
import org.sakaiproject.tool.assessment.services.assessment.EventLogService;
import org.sakaiproject.tool.assessment.services.assessment.PublishedAssessmentService;
import org.sakaiproject.tool.assessment.util.ExtendedTimeDeliveryService;
import org.sakaiproject.tool.assessment.util.SamigoExpressionError;
import org.sakaiproject.tool.assessment.util.SamigoExpressionParser;
import org.sakaiproject.tool.assessment.util.comparator.ImageMapGradingItemComparator;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
/**
* The GradingService calls the back end to get/store grading information.
* It also calculates scores for autograded types.
*/
@Slf4j
public class GradingService
{
/**
* Key for a complext numeric answer e.g. 9+9i
*/
public static final String ANSWER_TYPE_COMPLEX = "COMPLEX";
/**
* key for a real number representation e.g 1 or 10E5
*/
public static final String ANSWER_TYPE_REAL = "REAL";
// CALCULATED_QUESTION
public static final String OPEN_BRACKET = "\\{";
public static final String CLOSE_BRACKET = "\\}";
public static final String CALCULATION_OPEN = "[["; // not regex safe
public static final String CALCULATION_CLOSE = "]]"; // not regex safe
public static final char CALCULATION_AUX_OPEN = '└';
public static final char CALCULATION_AUX_CLOSE = '┐';
public static final String FORMAT_MASK = "0E0";
public static final BigDecimal DEFAULT_MAX_THRESHOLD = BigDecimal.valueOf(1.0e+11);
public static final BigDecimal DEFAULT_MIN_THRESHOLD = BigDecimal.valueOf(0.0001);
/**
* regular expression for matching the contents of a variable or formula name
* in Calculated Questions
* NOTE: Old regex: ([\\w\\s\\.\\-\\^\\$\\!\\&\\@\\?\\*\\%\\(\\)\\+=#`~&:;|,/<>\\[\\]\\\\\\'\"]+?)
* was way too complicated.
*/
public static final String CALCQ_VAR_FORM_NAME = "[a-zA-Z][^\\{\\}]*?"; // non-greedy (must start wtih alpha)
public static final String CALCQ_VAR_FORM_NAME_EXPRESSION = "("+CALCQ_VAR_FORM_NAME+")";
public static final String CALCQ_VAR_FORM_NAME_EXPRESSION_FORMATTED = OPEN_BRACKET + CALCQ_VAR_FORM_NAME_EXPRESSION + CLOSE_BRACKET;
// variable match - (? texts;
@Getter @Setter
private HashMap answersMap = new HashMap();
private static final int MAX_ERROR_TRIES = 100;
/**
* Get all scores for a published assessment from the back end.
*/
public List getTotalScores(String publishedId, String which)
{
return getTotalScores(publishedId, which, true);
}
public List getTotalScores(String publishedId, String which, boolean getSubmittedOnly)
{
try {
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getTotalScores(NumberUtils.createLong(publishedId), which, getSubmittedOnly);
} catch (Exception e) {
log.warn("Could not retrieve total scores for published assessment {}", publishedId, e);
}
return Collections.emptyList();
}
/**
* Get all submissions for a published assessment from the back end.
*/
public List getAllSubmissions(String publishedId)
{
List results = null;
try {
results = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getAllSubmissions(publishedId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public List getAllAssessmentGradingData(Long publishedId)
{
List results = null;
try {
results = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getAllAssessmentGradingData(publishedId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public List getHighestAssessmentGradingList(Long publishedId)
{
List results = null;
try {
results =
new ArrayList(PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getHighestAssessmentGradingList(publishedId));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public List getHighestSubmittedOrGradedAssessmentGradingList(Long publishedId)
{
List results = null;
try {
results =
new ArrayList(PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getHighestSubmittedOrGradedAssessmentGradingList(publishedId));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public List getLastAssessmentGradingList(Long publishedId)
{
List results = null;
try {
results =
new ArrayList(PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getLastAssessmentGradingList(publishedId));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public List getLastSubmittedAssessmentGradingList(Long publishedId)
{
List results = null;
try {
results =
PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getLastSubmittedAssessmentGradingList(publishedId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public List getLastSubmittedOrGradedAssessmentGradingList(Long publishedId)
{
List results = null;
try {
results =
PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getLastSubmittedOrGradedAssessmentGradingList(publishedId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public void saveTotalScores(List gdataList, PublishedAssessmentIfc pub)
{
try {
AssessmentGradingData gdata;
if (gdataList.size()>0)
gdata = (AssessmentGradingData) gdataList.get(0);
else return;
Integer scoringType = getScoringType(pub);
List oldList = getAssessmentGradingsByScoringType(scoringType, gdata.getPublishedAssessmentId());
for (int i=0; i0) {
gdata = (AssessmentGradingData) gdataList.get(0);
}
else {
return;
}
Integer scoringType = getScoringType(pub);
List fullList = getAssessmentGradingsByScoringType(
scoringType, gdata.getPublishedAssessmentId());
List l = new ArrayList();
for (int i=0; i< fullList.size(); i++){
AssessmentGradingData ag = (AssessmentGradingData)fullList.get(i);
if (ag.getAgentId().equals(studentId))
l.add(ag);
}
//When there is no more submission left for this student, update
//this student's grade on gradebook as null(the initial state).
if(l.isEmpty())
l.add(gdata);
notifyGradebook(l, pub);
} catch (GradebookServiceException ge) {
log.error(ge.getMessage(), ge);
throw ge;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
private List getListForGradebookNotification(List newList, List oldList) {
List l = new ArrayList();
Map h = new HashMap();
for (int i=0; i commentsA = Optional.ofNullable(a.getComments());
Optional commentsB = Optional.ofNullable(b.getComments());
if ( !commentsA.orElse("").equals(commentsB.orElse("")) ){
l.add(a);
}
}
}
return l;
}
public List getAssessmentGradingsByScoringType(
Integer scoringType, Long publishedAssessmentId){
List l;
// get the list of highest score
if ((scoringType).equals(EvaluationModelIfc.HIGHEST_SCORE)){
l = getHighestSubmittedOrGradedAssessmentGradingList(publishedAssessmentId);
}
// get the list of last score
else if ((scoringType).equals(EvaluationModelIfc.LAST_SCORE)) {
l = getLastSubmittedOrGradedAssessmentGradingList(publishedAssessmentId);
}
else {
l = getTotalScores(publishedAssessmentId.toString(), "3", false);
}
return new ArrayList(l);
}
public Integer getScoringType(PublishedAssessmentIfc pub){
Integer scoringType = null;
EvaluationModelIfc e = pub.getEvaluationModel();
if ( e!=null ){
scoringType = e.getScoringType();
}
return scoringType;
}
private boolean updateGradebook(AssessmentGradingData data, PublishedAssessmentIfc pub){
// no need to notify gradebook if this submission is not for grade
boolean forGrade = (Boolean.TRUE).equals(data.getForGrade());
boolean toGradebook = false;
EvaluationModelIfc e = pub.getEvaluationModel();
if ( e!=null ){
String toGradebookString = e.getToGradeBook();
toGradebook = toGradebookString.equals(EvaluationModelIfc.TO_DEFAULT_GRADEBOOK.toString());
}
return (forGrade && toGradebook);
}
private void notifyGradebook(List l, PublishedAssessmentIfc pub){
for (int i=0; i getMediaArray(String itemGradingId){
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getMediaArray(Long.valueOf(itemGradingId));
}
public List getMediaArray2(String itemGradingId){
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getMediaArray2(Long.valueOf(itemGradingId));
}
public List getMediaArray(ItemGradingData i){
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getMediaArray(i);
}
public Map> getMediaItemGradingHash(Long assessmentGradingId) {
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getMediaItemGradingHash(assessmentGradingId);
}
public List getMediaArray(String publishedId, String publishItemId, String which){
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getMediaArray(Long.valueOf(publishedId), Long.valueOf(publishItemId), which);
}
public ItemGradingData getLastItemGradingDataByAgent(String publishedItemId, String agentId)
{
try {
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getLastItemGradingDataByAgent(Long.valueOf(publishedItemId), agentId);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
public ItemGradingData getItemGradingData(String assessmentGradingId, String publishedItemId)
{
try {
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getItemGradingData(Long.valueOf(assessmentGradingId), Long.valueOf(publishedItemId));
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
public AssessmentGradingData load(String assessmentGradingId) {
return load(assessmentGradingId, true);
}
public AssessmentGradingData load(String assessmentGradingId, boolean loadGradingAttachment) {
try{
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
load(Long.valueOf(assessmentGradingId), loadGradingAttachment);
}
catch(Exception e)
{
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
public ItemGradingData getItemGrading(String itemGradingId) {
try{
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getItemGrading(Long.valueOf(itemGradingId));
}
catch(Exception e)
{
log.error(e.getMessage(), e);
throw new Error(e);
}
}
public AssessmentGradingData getLastAssessmentGradingByAgentId(String publishedAssessmentId, String agentIdString) {
try{
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getLastAssessmentGradingByAgentId(Long.valueOf(publishedAssessmentId), agentIdString);
}
catch(Exception e)
{
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
public AssessmentGradingData getLastSavedAssessmentGradingByAgentId(String publishedAssessmentId, String agentIdString) {
try{
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getLastSavedAssessmentGradingByAgentId(Long.valueOf(publishedAssessmentId), agentIdString);
}
catch(Exception e)
{
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
public AssessmentGradingData getLastSubmittedAssessmentGradingByAgentId(String publishedAssessmentId, String agentIdString, String assessmentGradingId) {
AssessmentGradingData assessmentGranding = null;
try {
if (assessmentGradingId != null) {
assessmentGranding = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getLastSubmittedAssessmentGradingByAgentId(Long.valueOf(publishedAssessmentId), agentIdString, Long.valueOf(assessmentGradingId));
}
else {
assessmentGranding = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getLastSubmittedAssessmentGradingByAgentId(Long.valueOf(publishedAssessmentId), agentIdString, null);
}
}
catch(Exception e)
{
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
return assessmentGranding;
}
public void saveItemGrading(ItemGradingData item)
{
try {
PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().saveItemGrading(item);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public void saveOrUpdateAssessmentGrading(AssessmentGradingData assessment)
{
try {
/*
// Comment out the whole IF section because the only thing we do here is to
// update the itemGradingSet. However, this update is redundant as it will
// be updated in saveOrUpdateAssessmentGrading(assessment).
if (assessment.getAssessmentGradingId()!=null
&& assessment.getAssessmentGradingId().longValue()>0){
//1. if assessmentGrading contain itemGrading, we want to insert/update itemGrading first
Set itemGradingSet = assessment.getItemGradingSet();
Iterator iter = itemGradingSet.iterator();
while (iter.hasNext()) {
ItemGradingData itemGradingData = (ItemGradingData) iter.next();
log.debug("date = " + itemGradingData.getSubmittedDate());
}
// The following line seems redundant. I cannot see a reason why we need to save the itmeGradingSet
// here and then again in following saveOrUpdateAssessmentGrading(assessment). Comment it out.
//saveOrUpdateAll(itemGradingSet);
}
*/
// this will update itemGradingSet and assessmentGrading. May as well, otherwise I would have
// to reload assessment again
PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().saveOrUpdateAssessmentGrading(assessment);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
// This API only touch SAM_ASSESSMENTGRADING_T. No data gets inserted/updated in SAM_ITEMGRADING_T
public void saveOrUpdateAssessmentGradingOnly(AssessmentGradingData assessment)
{
Set origItemGradingSet = assessment.getItemGradingSet();
HashSet h = new HashSet(origItemGradingSet);
// Clear the itemGradingSet so no data gets inserted/updated in SAM_ITEMGRADING_T;
origItemGradingSet.clear();
int size = assessment.getItemGradingSet().size();
log.debug("before persist to db: size = " + size);
try {
PersistenceService.getInstance().getAssessmentGradingFacadeQueries().saveOrUpdateAssessmentGrading(assessment);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
finally {
// Restore the original itemGradingSet back
assessment.setItemGradingSet(h);
size = assessment.getItemGradingSet().size();
log.debug("after persist to db: size = {}", size);
}
}
public List getAssessmentGradingIds(String publishedItemId){
try{
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getAssessmentGradingIds(Long.valueOf(publishedItemId));
}
catch(Exception e)
{
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
public AssessmentGradingData getHighestAssessmentGrading(String publishedAssessmentId, String agentId){
try{
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getHighestAssessmentGrading(Long.valueOf(publishedAssessmentId), agentId);
}
catch(Exception e)
{
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
public AssessmentGradingData getHighestSubmittedAssessmentGrading(String publishedAssessmentId, String agentId, String assessmentGradingId){
AssessmentGradingData assessmentGrading = null;
try {
if (assessmentGradingId != null) {
assessmentGrading = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getHighestSubmittedAssessmentGrading(Long.valueOf(publishedAssessmentId), agentId, Long.valueOf(assessmentGradingId));
}
else {
assessmentGrading = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getHighestSubmittedAssessmentGrading(Long.valueOf(publishedAssessmentId), agentId, null);
}
}
catch(Exception e)
{
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
return assessmentGrading;
}
public AssessmentGradingData getHighestSubmittedAssessmentGrading(String publishedAssessmentId, String agentId){
return getHighestSubmittedAssessmentGrading(publishedAssessmentId, agentId, null);
}
public Set getItemGradingSet(String assessmentGradingId){
try{
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getItemGradingSet(Long.valueOf(assessmentGradingId));
}
catch(Exception e){
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
public Map getAssessmentGradingByItemGradingId(String publishedAssessmentId){
try{
return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getAssessmentGradingByItemGradingId(Long.valueOf(publishedAssessmentId));
}
catch(Exception e){
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
public void updateItemScore(ItemGradingData gdata, double scoreDifference, PublishedAssessmentIfc pub){
try {
AssessmentGradingData adata = load(gdata.getAssessmentGradingId().toString());
adata.setItemGradingSet(getItemGradingSet(adata.getAssessmentGradingId().toString()));
Set itemGradingSet = adata.getItemGradingSet();
Iterator iter = itemGradingSet.iterator();
double totalAutoScore = 0;
double totalOverrideScore = adata.getTotalOverrideScore();
while (iter.hasNext()){
ItemGradingData i = (ItemGradingData)iter.next();
if (i.getItemGradingId().equals(gdata.getItemGradingId())){
i.setAutoScore(gdata.getAutoScore());
i.setComments(gdata.getComments());
i.setGradedBy(AgentFacade.getAgentString());
i.setGradedDate(new Date());
}
if (i.getAutoScore()!=null)
totalAutoScore += i.getAutoScore();
}
adata.setTotalAutoScore(totalAutoScore);
if (Double.compare((totalAutoScore+totalOverrideScore),Double.valueOf("0"))<0){
adata.setFinalScore(Double.valueOf("0"));
}else{
adata.setFinalScore(totalAutoScore+totalOverrideScore);
}
saveOrUpdateAssessmentGrading(adata);
if (scoreDifference != 0){
notifyGradebookByScoringType(adata, pub);
}
} catch (GradebookServiceException ge) {
log.error(ge.getMessage(), ge);
throw ge;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
/**
* Assume this is a new item.
*/
public void storeGrades(AssessmentGradingData data, PublishedAssessmentIfc pub,
Map publishedItemHash, Map publishedItemTextHash,
Map publishedAnswerHash, Map invalidFINMap, List invalidSALengthList) throws GradebookServiceException, FinFormatException
{
log.debug("storeGrades: data.getSubmittedDate()" + data.getSubmittedDate());
storeGrades(data, false, pub, publishedItemHash, publishedItemTextHash, publishedAnswerHash, true, invalidFINMap, invalidSALengthList);
}
/**
* Assume this is a new item.
*/
public void storeGrades(AssessmentGradingData data, PublishedAssessmentIfc pub,
Map publishedItemHash, Map publishedItemTextHash,
Map publishedAnswerHash, boolean persistToDB, Map invalidFINMap, List invalidSALengthList) throws GradebookServiceException, FinFormatException
{
log.debug("storeGrades (not persistToDB) : data.getSubmittedDate()" + data.getSubmittedDate());
storeGrades(data, false, pub, publishedItemHash, publishedItemTextHash, publishedAnswerHash, persistToDB, invalidFINMap, invalidSALengthList);
}
public void storeGrades(AssessmentGradingData data, boolean regrade, PublishedAssessmentIfc pub,
Map publishedItemHash, Map publishedItemTextHash,
Map publishedAnswerHash, boolean persistToDB) throws GradebookServiceException, FinFormatException {
log.debug("storeGrades (not persistToDB) : data.getSubmittedDate()" + data.getSubmittedDate());
storeGrades(data, regrade, pub, publishedItemHash, publishedItemTextHash, publishedAnswerHash, persistToDB, null, null);
}
/**
* This is the big, complicated mess where we take all the items in
* an assessment, store the grading data, auto-grade it, and update
* everything.
*
* If regrade is true, we just recalculate the graded score. If it's
* false, we do everything from scratch.
*/
public void storeGrades(AssessmentGradingData data, boolean regrade, PublishedAssessmentIfc pub,
Map publishedItemHash, Map publishedItemTextHash,
Map publishedAnswerHash, boolean persistToDB, Map invalidFINMap, List invalidSALengthList)
throws GradebookServiceException, FinFormatException {
log.debug("****x1. regrade ="+regrade+" "+(new Date()).getTime());
try {
String agent = data.getAgentId();
// note that this itemGradingSet is a partial set of answer submitted. it contains only
// newly submitted answers, updated answers and MCMR/FIB/FIN answers ('cos we need the old ones to
// calculate scores for new ones)
Set itemGradingSet = data.getItemGradingSet();
if (itemGradingSet == null)
itemGradingSet = new HashSet<>();
log.debug("****itemGrading size="+itemGradingSet.size());
List tempItemGradinglist = new ArrayList<>(itemGradingSet);
// CALCULATED_QUESTION - if this is a calc question. Carefully sort the list of answers
if (isCalcQuestion(tempItemGradinglist, publishedItemHash)) {
Collections.sort(tempItemGradinglist, new Comparator(){
public int compare(ItemGradingData o1, ItemGradingData o2) {
ItemGradingData gradeData1 = o1;
ItemGradingData gradeData2 = o2;
// protect against blank ones in samigo initial setup.
if (gradeData1 == null) return -1;
if (gradeData2 == null) return 1;
if (gradeData1.getPublishedAnswerId() == null) return -1;
if (gradeData2.getPublishedAnswerId() == null) return 1;
return gradeData1.getPublishedAnswerId().compareTo(gradeData2.getPublishedAnswerId());
}
});
}
//IMAGEMAP_QUESTION - order by itemGradingId if it is an imageMap question
if (isImageMapQuestion(tempItemGradinglist, publishedItemHash)) {
tempItemGradinglist.sort(new ImageMapGradingItemComparator());
}
Iterator iter = tempItemGradinglist.iterator();
// fibEmiAnswersMap contains a map of HashSet of answers for a FIB or EMI item,
// key =itemid, value= HashSet of answers for each item.
// For FIB: This is used to keep track of answers we have already used for
// mutually exclusive multiple answer type of FIB, such as
// The flag of the US is {red|white|blue},{red|white|blue}, and {red|white|blue}.
// so if the first blank has an answer 'red', the 'red' answer should
// not be included in the answers for the other mutually exclusive blanks.
// For EMI: This keeps track of how many answers were given so we don't give
// extra marks for to many answers.
Map fibEmiAnswersMap = new HashMap();
Map>> emiScoresMap = new HashMap<>();
//change algorithm based on each question (SAK-1930 & IM271559) -cwen
Map totalItems = new HashMap();
log.debug("****x2. {}", (new Date()).getTime());
double autoScore;
Long itemId = (long)0;
Long previousItemId = (long)0;
int calcQuestionAnswerSequence = 1; // sequence of answers for CALCULATED_QUESTION
boolean imageMapAlreadyOk = true;
boolean neededAllOk = false;
for(ItemGradingData itemGrading: tempItemGradinglist){
// CALCULATED_QUESTION - We increment this so we that calculated
// questions can know where we are in the sequence of answers.
if (itemGrading.getPublishedItemId().equals(itemId)) {
calcQuestionAnswerSequence++;
}
else {
calcQuestionAnswerSequence = 1;
}
itemId = itemGrading.getPublishedItemId();
ItemDataIfc item = (ItemDataIfc) publishedItemHash.get(itemId);
if (item == null) {
//this probably shouldn't happen
log.error("unable to retrive itemDataIfc for: {}", publishedItemHash.get(itemId));
continue;
}
Long itemType = item.getTypeId();
/*
* IMAGEMAP_QUESTIONS can have some items for same question, so we need to remember
* between iterations if we have some wrong items in a question that has more than one item.
*/
if(TypeIfc.IMAGEMAP_QUESTION.equals(itemType) && !itemId.equals(previousItemId)) {
previousItemId = itemId;
imageMapAlreadyOk = true;
}
for (ItemMetaDataIfc meta : item.getItemMetaDataSet())
{
if (meta.getLabel().equals(ItemMetaDataIfc.REQUIRE_ALL_OK))
{
if (meta.getEntry().equals("true"))
{
neededAllOk = true;
break;
}
if (meta.getEntry().equals("false"))
{
neededAllOk = false;
break;
}
}
}
itemGrading.setAssessmentGradingId(data.getAssessmentGradingId());
//itemGrading.setSubmittedDate(new Date());
itemGrading.setAgentId(agent);
itemGrading.setOverrideScore(Double.valueOf(0));
if (itemType == 5 && itemGrading.getAnswerText() != null) {
String processedAnswerText = itemGrading.getAnswerText().replaceAll("\r", "").replaceAll("\n", "");
if (processedAnswerText.length() > 32000) {
if (invalidSALengthList != null) {
invalidSALengthList.add(item.getItemId());
}
}
}
// note that totalItems & fibAnswersMap would be modified by the following method
try {
autoScore = getScoreByQuestionType(itemGrading, item, itemType, publishedItemTextHash,
totalItems, fibEmiAnswersMap, emiScoresMap, publishedAnswerHash, regrade, calcQuestionAnswerSequence);
}
catch (FinFormatException e) {
log.warn("Fin Format Exception while processing response. ", e);
autoScore = 0d;
if (invalidFINMap != null) {
if (invalidFINMap.containsKey(itemId)) {
List list = (ArrayList) invalidFINMap.get(itemId);
list.add(itemGrading.getItemGradingId());
}
else {
List list = new ArrayList();
list.add(itemGrading.getItemGradingId());
invalidFINMap.put(itemId, list);
}
}
}
if (TypeIfc.IMAGEMAP_QUESTION.equals(itemType) && neededAllOk){
if(!imageMapAlreadyOk) {
autoScore = 0;
}else if(autoScore == WRONG_IMAGE_MAP_ANSWER_NON_PARCIAL){
autoScore = 0;
imageMapAlreadyOk = false;
//itemGradingSet have items with positive score if the actual answer isnt the first of the question
for(ItemGradingData imageMapItem: itemGradingSet) {
if(imageMapItem.getPublishedItemId().equals(itemId))
imageMapItem.setAutoScore(0.0);
}
}
}
log.debug("**!regrade, autoScore="+autoScore);
if (!(TypeIfc.MULTIPLE_CORRECT).equals(itemType) && !(TypeIfc.EXTENDED_MATCHING_ITEMS).equals(itemType))
totalItems.put(itemId, autoScore);
if (regrade && TypeIfc.AUDIO_RECORDING.equals(itemType))
itemGrading.setAttemptsRemaining(item.getTriesAllowed());
itemGrading.setAutoScore(autoScore);
}
if ((invalidFINMap != null && invalidFINMap.size() > 0) || (invalidSALengthList != null && invalidSALengthList.size() > 0)) {
return;
}
// Added persistToDB because if we don't save data to DB later, we shouldn't update the assessment
// submittedDate either. The date should be sync in delivery bean and DB
// This is for DeliveryBean.checkDataIntegrity()
if (!regrade && persistToDB)
{
data.setSubmittedDate(new Date());
setIsLate(data, pub);
}
log.debug("****x3. {}", (new Date()).getTime());
List emiItemGradings = new ArrayList<>();
// the following procedure ensure total score awarded per question is no less than 0
// this probably only applies to MCMR question type - daisyf
iter = itemGradingSet.iterator();
//since the itr goes through each answer (multiple answers for a signle mc question), keep track
//of its total score by itemId -> autoScore[]{user's score, total possible}
Map mcmcAllOrNothingCheck = new HashMap<>();
//collect min score information to determine if the auto score will need to be adjusted
//since there can be multiple questions store in map: itemId -> {user's score, minScore, # of answers}
Map minScoreCheck = new HashMap<>();
Map minScoreAnswered = new HashMap<>();
double totalAutoScoreCheck = 0;
Map countMcmcAllItemGradings = new HashMap<>();
//get item information to check if it's MCMS and Not Partial Credit
Long itemType2;
String mcmsPartialCredit;
double itemScore = -1;
while(iter.hasNext())
{
ItemGradingData itemGrading = iter.next();
itemId = itemGrading.getPublishedItemId();
ItemDataIfc item = (ItemDataIfc) publishedItemHash.get(itemId);
//SAM-1724 it's possible the item is not in the hash -DH
if (item == null) {
log.error("unable to retrive itemDataIfc for: " + publishedItemHash.get(itemId));
continue;
}
itemType2 = item.getTypeId();
//get item information to check if it's MCMS and Not Partial Credit
mcmsPartialCredit = item.getItemMetaDataByLabel(ItemMetaDataIfc.MCMS_PARTIAL_CREDIT);
itemScore = item.getScore();
//double autoScore = (double) 0;
// this does not apply to EMI
// just create a short-list and handle differently below
if ((TypeIfc.EXTENDED_MATCHING_ITEMS).equals(itemType2)) {
emiItemGradings.add(itemGrading);
continue;
}
double eachItemScore = ((Double) totalItems.get(itemId));
if((eachItemScore < 0) && !((TypeIfc.MULTIPLE_CHOICE).equals(itemType2)||(TypeIfc.TRUE_FALSE).equals(itemType2)||(TypeIfc.MULTIPLE_CORRECT_SINGLE_SELECTION).equals(itemType2)))
{
itemGrading.setAutoScore( Double.valueOf(0));
}
//keep track of MCMC answer's total score in order to check for all or nothing
if(TypeIfc.MULTIPLE_CORRECT.equals(itemType2) && "false".equals(mcmsPartialCredit)){
Double accumulatedScore = itemGrading.getAutoScore();
if(mcmcAllOrNothingCheck.containsKey(itemId)){
Double[] accumulatedScoreArr = mcmcAllOrNothingCheck.get(itemId);
accumulatedScore += accumulatedScoreArr[0];
}
mcmcAllOrNothingCheck.put(itemId, new Double[]{accumulatedScore, item.getScore()});
int count = 0;
if(countMcmcAllItemGradings.containsKey(itemId))
count = (countMcmcAllItemGradings.get(itemId));
countMcmcAllItemGradings.put(itemId, ++count);
}
//min score check
if(item.getMinScore() != null){
Double accumulatedScore = itemGrading.getAutoScore();
Double itemParts = 1d;
boolean answered = isAnswered(itemGrading, itemType2);
if(minScoreCheck.containsKey(itemId)){
Double[] accumulatedScoreArr = minScoreCheck.get(itemId);
accumulatedScore += accumulatedScoreArr[0];
itemParts += accumulatedScoreArr[2];
answered = answered || minScoreAnswered.get(itemId);
}
minScoreCheck.put(itemId, new Double[]{accumulatedScore, item.getMinScore(), itemParts});
minScoreAnswered.put(itemId, answered);
}
}
log.debug("****x3.1 {}", (new Date()).getTime());
// Loop 1: this procedure ensure total score awarded per EMI item
// is correct
// For emi's there are multiple gradings per item per question,
// for the grading we only know scores after grading so we need
// to reset the grading score here to the correct scores
// this currently only applies to EMI question type
if (!emiItemGradings.isEmpty()) {
Map>> emiOrderedScoresMap = reorderEMIScoreMap(emiScoresMap);
iter = emiItemGradings.iterator();
while (iter.hasNext()) {
ItemGradingData itemGrading = iter.next();
//SAM-2016 check for Nullity
if (itemGrading == null) {
log.warn("Map contains null itemgrading!");
continue;
}
Map> innerMap = emiOrderedScoresMap
.get(itemGrading.getPublishedItemId());
if (innerMap == null) {
log.warn("Inner map is empty!");
continue;
}
Map scoreMap = innerMap
.get(itemGrading.getPublishedItemTextId());
if (scoreMap == null) {
log.warn("Score map is empty!");
continue;
}
EMIScore score = scoreMap
.get(itemGrading.getPublishedAnswerId());
if (score == null) {
//its possible! SAM-2016
log.warn("we can't find a score for answer: {}", itemGrading.getPublishedAnswerId());
continue;
}
itemGrading.setAutoScore(emiOrderedScoresMap
.get(itemGrading.getPublishedItemId())
.get(itemGrading.getPublishedItemTextId())
.get(itemGrading.getPublishedAnswerId()).effectiveScore);
}
totalAutoScoreCheck = 0;
}
// if it's MCMS and Not Partial Credit and the score isn't 100% (totalAutoScoreCheck != itemScore),
// that means the user didn't answer all of the correct answers only.
// We need to set their score to 0 for all ItemGrading items
for(Entry entry : mcmcAllOrNothingCheck.entrySet()){
if(!Precision.equalsIncludingNaN(entry.getValue()[0], entry.getValue()[1], 0.001d)) {
//reset all scores to 0 since the user didn't get all correct answers
iter = itemGradingSet.iterator();
while(iter.hasNext()){
ItemGradingData itemGrading = iter.next();
Long itemId2 = entry.getKey();
if(itemGrading.getPublishedItemId().equals(itemId2)){
AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(itemGrading.getPublishedAnswerId());
if (answer == null) {
itemGrading.setAutoScore(Double.valueOf(0));
log.error("unable to retrieve answerIfc for: " + itemId2);
continue;
}
if(!countMcmcAllItemGradings.containsKey(itemId2)){
itemGrading.setAutoScore(Double.valueOf(0));
log.error("unable to retrieve itemGrading's counter for: " + itemId2);
continue;
}
double discount = (Math.abs(answer.getDiscount()) * ((double) -1));
int count = (countMcmcAllItemGradings.get(itemId2));
double itemGrDisc = discount/count;
itemGrading.setAutoScore(itemGrDisc);
}
}
}
}
// If there is a minimum score value, then make sure the auto score is at least the minimum
// entry.getValue()[0] = total score for the question
// entry.getValue()[1] = min score
// entry.getValue()[2] = how many question answers to divide minScore across
for (Entry entry : minScoreCheck.entrySet()) {
if (entry.getValue()[0] <= entry.getValue()[1]) {
//reset all scores to 0 since the user didn't get all correct answers
for (ItemGradingData itemGrading : itemGradingSet) {
ItemDataIfc item = (ItemDataIfc) publishedItemHash.get(itemGrading.getPublishedItemId());
if (!Boolean.valueOf(item.getPartialCreditFlag())) {
// We should only set the autoScore to the min score
// if partial credit is not in effect
if (minScoreAnswered.get(entry.getKey()) && itemGrading.getPublishedItemId().equals(entry.getKey())) {
itemGrading.setAutoScore(entry.getValue()[1]/entry.getValue()[2]);
}
}
}
}
}
log.debug("****x4. {}", Instant.now().toEpochMilli());
// save#1: this itemGrading Set is a partial set of answers submitted. it contains new answers and
// updated old answers and FIB answers ('cos we need the old answer to calculate the score for new
// ones). we need to be cheap, we don't want to update record that hasn't been
// changed. Yes, assessmentGrading's total score will be out of sync at this point, I am afraid. It
// would be in sync again once the whole method is completed sucessfully.
if (persistToDB) {
saveOrUpdateAll(itemGradingSet);
}
log.debug("****x5. "+(new Date()).getTime());
// save#2: now, we need to get the full set so we can calculate the total score accumulate for the
// whole assessment.
Set fullItemGradingSet = getItemGradingSet(data.getAssessmentGradingId().toString());
double totalAutoScore = getTotalAutoScore(fullItemGradingSet);
data.setTotalAutoScore(totalAutoScore);
if (Double.compare((totalAutoScore + data.getTotalOverrideScore()), new Double("0"))<0){
data.setFinalScore( Double.valueOf("0"));
}else{
data.setFinalScore(totalAutoScore + data.getTotalOverrideScore());
}
log.debug("****x6. "+(new Date()).getTime());
} catch (GradebookServiceException ge) {
log.error(ge.getMessage(), ge);
throw ge;
}
catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
// save#3: itemGradingSet has been saved above so just need to update assessmentGrading
// therefore setItemGradingSet as empty first - daisyf
// however, if we do not persit to DB, we want to keep itemGradingSet with data for later use
// Because if itemGradingSet is not saved to DB, we cannot go to DB to get it. We have to
// get it through data.
if (persistToDB) {
data.setItemGradingSet(new HashSet());
saveOrUpdateAssessmentGrading(data);
log.debug("****x7. {}", (new Date()).getTime());
if (!regrade) {
notifyGradebookByScoringType(data, pub);
}
}
log.debug("****x8. {}", (new Date()).getTime());
// I am not quite sure what the following code is doing... I modified this based on my assumption:
// If this happens dring regrade, we don't want to clean these data up
// We only want to clean them out in delivery
if (!regrade && Boolean.TRUE.equals(data.getForGrade())) {
// remove the assessmentGradingData created during gradiing (by updatding total score page)
removeUnsubmittedAssessmentGradingData(data);
}
}
private double getTotalAutoScore(Set itemGradingSet){
BigDecimal totalAutoScore = BigDecimal.ZERO;
for(ItemGradingData itemGradingData : (Set) itemGradingSet){
if (itemGradingData.getAutoScore()!=null){
totalAutoScore = totalAutoScore.add(new BigDecimal(itemGradingData.getAutoScore()));
}
}
return totalAutoScore.doubleValue();
}
private boolean isAnswered(ItemGradingData data, Long type) {
if(TypeIfc.MATCHING.equals(type)) {
if (data.getPublishedAnswerId() != null) {
return true;
}
} else if(TypeIfc.ESSAY_QUESTION.equals(type) || TypeIfc.FILL_IN_BLANK.equals(type) || TypeIfc.FILL_IN_NUMERIC.equals(type)) {
if (StringUtils.isNotEmpty(data.getAnswerText())) {
return true;
}
} else if(TypeIfc.IMAGEMAP_QUESTION.equals(type)) {
if (StringUtils.isNotEmpty(data.getAnswerText()) && data.getAnswerText().matches("\\{\"x\":-?\\d+,\"y\":-?\\d+\\}")) {
return true;
}
} else {
if (data.getPublishedAnswerId() != null || StringUtils.isNotEmpty(data.getAnswerText())) {
return true;
}
}
return false;
}
public void notifyGradebookByScoringType(AssessmentGradingData data, PublishedAssessmentIfc pub){
if (pub == null || pub.getEvaluationModel() == null) {
// should not come to here
log.warn("publishedAssessment is null or publishedAssessment.getEvaluationModel() is null");
return;
}
Integer scoringType = pub.getEvaluationModel().getScoringType();
if (updateGradebook(data, pub)){
AssessmentGradingData d = data; // data is the last submission
// need to decide what to tell gradebook
if ((scoringType).equals(EvaluationModelIfc.HIGHEST_SCORE)) {
// If this next call comes back null, don't overwrite our real AG with a null one
final AssessmentGradingData highestAG = getHighestSubmittedAssessmentGrading(pub.getPublishedAssessmentId().toString(), data.getAgentId());
if (highestAG != null) {
d = highestAG;
}
}
// Send the average score if average was selected for multiple submissions
else if (scoringType.equals(EvaluationModelIfc.AVERAGE_SCORE)) {
// status = 5: there is no submission but grader update something in the score page
if(data.getStatus() == AssessmentGradingData.NO_SUBMISSION) {
d.setFinalScore(data.getFinalScore());
} else {
Double averageScore = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().
getAverageSubmittedAssessmentGrading(pub.getPublishedAssessmentId(), data.getAgentId());
d.setFinalScore(averageScore);
}
}
notifyGradebook(d, pub);
}
}
private double getScoreByQuestionType(ItemGradingData itemGrading, ItemDataIfc item,
Long itemType, Map publishedItemTextHash,
Map totalItems, Map fibAnswersMap, Map>> emiScoresMap,
Map publishedAnswerHash, boolean regrade,
int calcQuestionAnswerSequence) throws FinFormatException {
//double score = (double) 0;
double initScore;
double autoScore = (double) 0;
double accumelateScore;
Long itemId = item.getItemId();
int type = itemType.intValue();
switch (type){
case 1: // MC Single Correct
if(item.getPartialCreditFlag())
autoScore = getAnswerScoreMCQ(itemGrading, publishedAnswerHash);
else{
autoScore = getAnswerScore(itemGrading, publishedAnswerHash);
}
//overridescore
if (itemGrading.getOverrideScore() != null)
autoScore += itemGrading.getOverrideScore();
totalItems.put(itemId, autoScore);
break;// MC Single Correct
case 12: // MC Multiple Correct Single Selection
case 3: // MC Survey
case 4: // True/False
autoScore = getAnswerScore(itemGrading, publishedAnswerHash);
//overridescore
if (itemGrading.getOverrideScore() != null)
autoScore += itemGrading.getOverrideScore();
totalItems.put(itemId, autoScore);
break;
case 2: // MC Multiple Correct
ItemTextIfc itemText = (ItemTextIfc) publishedItemTextHash.get(itemGrading.getPublishedItemTextId());
List answerArray = itemText.getAnswerArray();
int correctAnswers = 0;
if (answerArray != null){
for (int i =0; i 0) {
autoScore = initScore / item.getItemTextArraySorted().size();
} else {
autoScore = initScore;
}
//overridescore?
if (itemGrading.getOverrideScore() != null)
autoScore += itemGrading.getOverrideScore();
if (!totalItems.containsKey(itemId))
totalItems.put(itemId, autoScore);
else {
accumelateScore = ((Double)totalItems.get(itemId));
accumelateScore += autoScore;
totalItems.put(itemId, accumelateScore);
}
break;
case 8: // FIB
autoScore = getFIBScore(itemGrading, fibAnswersMap, item, publishedAnswerHash) / (double) ((ItemTextIfc) item.getItemTextSet().toArray()[0]).getAnswerSet().size();
//overridescore - cwen
if (itemGrading.getOverrideScore() != null)
autoScore += itemGrading.getOverrideScore();
if (!totalItems.containsKey(itemId))
totalItems.put(itemId, autoScore);
else {
accumelateScore = ((Double)totalItems.get(itemId));
accumelateScore += autoScore;
totalItems.put(itemId, accumelateScore);
}
break;
case 15: // CALCULATED_QUESTION
case 11: // FIN
try {
if (type == 15) { // CALCULATED_QUESTION
Map calculatedAnswersMap = getCalculatedAnswersMap(itemGrading, item, calcQuestionAnswerSequence);
int numAnswers = calculatedAnswersMap.size();
autoScore = getCalcQScore(itemGrading, item, calculatedAnswersMap, calcQuestionAnswerSequence ) / (double) numAnswers;
} else {
autoScore = getFINScore(itemGrading, item, publishedAnswerHash) / (double) ((ItemTextIfc) item.getItemTextSet().toArray()[0]).getAnswerSet().size();
}
}
catch (FinFormatException e) {
throw e;
}
//overridescore - cwen
if (itemGrading.getOverrideScore() != null)
autoScore += itemGrading.getOverrideScore();
if (!totalItems.containsKey(itemId))
totalItems.put(itemId, autoScore);
else {
accumelateScore = (Double) totalItems.get(itemId);
accumelateScore += autoScore;
totalItems.put(itemId, accumelateScore);
}
break;
case 14: // EMI
autoScore = getEMIScore(itemGrading, itemId, totalItems, emiScoresMap, publishedItemTextHash, publishedAnswerHash);
break;
case 5: // SAQ
case 6: // file upload
case 7: // audio recording
//overridescore - cwen
if (regrade && itemGrading.getAutoScore() != null) {
autoScore = itemGrading.getAutoScore();
}
if (itemGrading.getOverrideScore() != null)
autoScore += itemGrading.getOverrideScore();
if (!totalItems.containsKey(itemId))
totalItems.put(itemId, autoScore);
else {
accumelateScore = (Double) totalItems.get(itemId);
accumelateScore += autoScore;
totalItems.put(itemId, accumelateScore);
}
break;
case 16:
initScore = getImageMapScore(itemGrading,item, publishedItemTextHash,publishedAnswerHash);
//if one answer is 0 or negative, and need all OK to be scored, then autoScore=WRONG_IMAGE_MAP_ANSWER_NON_PARCIAL
//and we break the case...
boolean neededAllOk = false;
Iterator i = item.getItemMetaDataSet().iterator();
while (i.hasNext())
{
ItemMetaDataIfc meta = (ItemMetaDataIfc) i.next();
if (meta.getLabel().equals(ItemMetaDataIfc.REQUIRE_ALL_OK))
{
if (meta.getEntry().equals("true"))
{
neededAllOk = true;
break;
}
}
}
if (neededAllOk && (initScore <= 0)){
autoScore = WRONG_IMAGE_MAP_ANSWER_NON_PARCIAL;
break;
}
//if (initScore > 0) {
autoScore += initScore ;
//}
//overridescore?
if (itemGrading.getOverrideScore() != null)
autoScore += itemGrading.getOverrideScore();
if (!totalItems.containsKey(itemId)){
totalItems.put(itemId, autoScore);
}else {
accumelateScore = (Double) totalItems.get(itemId);
accumelateScore += autoScore;
totalItems.put(itemId, accumelateScore);
}
break;
}
return autoScore;
}
private boolean isThisItemDistractor(ItemDataIfc item, ItemGradingData thisItemGradingData) {
for (ItemTextIfc curItem : item.getItemTextArraySorted()) {
if (isDistractor(curItem) && curItem.getId().equals(thisItemGradingData.getPublishedItemTextId())) {
return true;
}
}
return false;
}
private double getScoreDistractor(ItemDataIfc item, ItemGradingData thisItemGradingData) {
if (thisItemGradingData.getPublishedAnswerId() == null) {
return 0;
} else if (thisItemGradingData.getPublishedAnswerId() < 0) {
return item.getScore();
} else {
return 0;
}
}
/**
* This grades multiple choice and true false questions. Since
* multiple choice/multiple select has a separate ItemGradingData for
* each choice, they're graded the same way the single choice are.
* Choices should be given negative score values if one wants them
* to lose points for the wrong choice.
*/
public double getAnswerScore(ItemGradingData data, Map publishedAnswerHash)
{
AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
if (answer == null || answer.getScore() == null) {
return (double) 0;
}
ItemDataIfc item = (ItemDataIfc) answer.getItem();
Long itemType = item.getTypeId();
if (answer.getIsCorrect() == null || !answer.getIsCorrect())
{
// return (double) 0;
// Para que descuente (For discount)
double score = (double) 0;
if ((TypeIfc.EXTENDED_MATCHING_ITEMS).equals(itemType)||(TypeIfc.MULTIPLE_CHOICE).equals(itemType)||(TypeIfc.TRUE_FALSE).equals(itemType)||(TypeIfc.MULTIPLE_CORRECT_SINGLE_SELECTION).equals(itemType)){
score = Math.abs(answer.getDiscount()) * ((double) -1);
}
answer.setPartialCredit(score);
return score;
}
return answer.getScore();
}
public void updateAutosubmitEventLog(AssessmentGradingData adata) {
EventLogService eventService = new EventLogService();
EventLogFacade eventLogFacade = new EventLogFacade();
Long gradingId = adata.getAssessmentGradingId();
List eventLogDataList = eventService.getEventLogData(gradingId);
if (!eventLogDataList.isEmpty()) {
EventLogData eventLogData= (EventLogData) eventLogDataList.get(0);
//will do the i18n issue later.
eventLogData.setErrorMsg("no_error_auto_submit");
final Date endDate = adata.getSubmittedDate() != null ? adata.getSubmittedDate() : new Date();
eventLogData.setEndDate(endDate);
if(eventLogData.getStartDate() != null) {
double minute= 1000*60;
int eclipseTime = (int)Math.ceil(((endDate.getTime() - eventLogData.getStartDate().getTime())/minute));
eventLogData.setEclipseTime(eclipseTime);
} else {
eventLogData.setEclipseTime(null);
eventLogData.setErrorMsg("error_auto_submit");
}
eventLogFacade.setData(eventLogData);
eventService.saveOrUpdateEventLog(eventLogFacade);
}
EventTrackingService.post(EventTrackingService.newEvent(SamigoConstants.EVENT_AUTO_SUBMIT_JOB,
AutoSubmitAssessmentsJob.safeEventLength("publishedAssessmentId=" + adata.getPublishedAssessmentId() +
", assessmentGradingId=" + gradingId), true));
Map notiValues = new HashMap<>();
notiValues.put("publishedAssessmentID", adata.getPublishedAssessmentId());
notiValues.put("assessmentGradingID", gradingId);
notiValues.put("userID", adata.getAgentId());
notiValues.put("submissionDate", adata.getSubmittedDate());
String confirmationNumber = adata.getAssessmentGradingId() + "-" + adata.getPublishedAssessmentId() + "-"
+ adata.getAgentId() + "-" + adata.getSubmittedDate().getTime();
notiValues.put( "confirmationNumber", confirmationNumber );
EventTrackingService.post(EventTrackingService.newEvent(SamigoConstants.EVENT_ASSESSMENT_SUBMITTED_AUTO, notiValues.toString(), AgentFacade.getCurrentSiteId(), false, SamigoConstants.NOTI_EVENT_ASSESSMENT_SUBMITTED));
}
private void notifyGradebook(AssessmentGradingData data, PublishedAssessmentIfc pub) throws GradebookServiceException {
// If the assessment is published to the gradebook, make sure to update the scores in the gradebook
String toGradebook = pub.getEvaluationModel().getToGradeBook();
org.sakaiproject.grading.api.GradingService g = null;
boolean integrated = IntegrationContextFactory.getInstance().isIntegrated();
if (integrated)
{
g = (org.sakaiproject.grading.api.GradingService) SpringBeanLocator.getInstance().
getBean("org.sakaiproject.grading.api.GradingService");
}
GradebookServiceHelper gbsHelper =
IntegrationContextFactory.getInstance().getGradebookServiceHelper();
PublishedAssessmentService publishedAssessmentService = new PublishedAssessmentService();
String currentSiteId = publishedAssessmentService.getPublishedAssessmentSiteId(pub.getPublishedAssessmentId().toString());
if (toGradebook.equals(EvaluationModelIfc.TO_DEFAULT_GRADEBOOK.toString())){
if(log.isDebugEnabled()) log.debug("Attempting to update a score in the gradebook");
// add retry logic to resolve deadlock problem while sending grades to gradebook
Double originalFinalScore = data.getFinalScore();
int retryCount = PersistenceService.getInstance().getPersistenceHelper().getRetryCount();
while (retryCount > 0){
try {
gbsHelper.updateExternalAssessmentScore(data, g);
retryCount = 0;
}
catch (org.sakaiproject.grading.api.AssessmentNotFoundException ante) {
log.warn("problem sending grades to gradebook: {}", ante.getMessage());
if (AssessmentIfc.RETRACT_FOR_EDIT_STATUS.equals(pub.getStatus())) {
retryCount = retry(retryCount, ante, pub, true);
}
else {
// Otherwise, do the same exeption handling as others
retryCount = retry(retryCount, ante, pub, false);
}
}
catch (Exception e) {
retryCount = retry(retryCount, e, pub, false);
}
}
// change the final score back to the original score since it may set to average score.
// if we're deleting the last submission, the score might be null bugid 5440
if(originalFinalScore != null && data.getFinalScore() != null && !(Precision.equalsIncludingNaN(data.getFinalScore(), originalFinalScore, 0.0001))) {
data.setFinalScore(originalFinalScore);
}
try {
Long publishedAssessmentId = data.getPublishedAssessmentId();
String agent = data.getAgentId();
String comment = data.getComments();
gbsHelper.updateExternalAssessmentComment(publishedAssessmentId, agent, comment, g);
}
catch (Exception ex) {
log.warn("Error sending comments to gradebook: {}", ex.getMessage());
}
} else {
log.debug("Not updating the gradebook. toGradebook = {}", toGradebook);
}
}
private int retry(int retryCount, Exception e, PublishedAssessmentIfc pub, boolean retractForEditStatus) {
log.warn("retrying...sending grades to gradebook. ");
log.warn("retry....");
retryCount--;
try {
int deadlockInterval = PersistenceService.getInstance().getPersistenceHelper().getDeadlockInterval();
Thread.sleep(deadlockInterval);
}
catch(InterruptedException ex){
log.warn(ex.getMessage());
}
if (retryCount==0) {
if (retractForEditStatus) {
// This happens in following scenario:
// 1. The assessment is active and has "None" for GB setting
// 2. Instructor retracts it for edit and update the to "Send to GB"
// 3. Instructor updates something on the total Score page
// Because the GB will not be created until the assessment gets republished,
// "AssessmentNotFoundException" will be thrown here. Since, this is the expected
// exception, we simply log a debug message without retrying or notifying the user.
// Of course, you can argue about what if the assessment gets deleted by other cause.
// But I would say the major cause would be this "retract" scenario. Also, without knowing
// the change history of the assessment, I think this is the best handling.
log.info("We quietly sallow the AssessmentNotFoundException excption here. Published Assessment Name: " + pub.getTitle());
}
else {
// after retries, still failed updating gradebook
log.warn("After all retries, still failed ... Now throw error to UI");
throw new GradebookServiceException(e);
}
}
return retryCount;
}
/**
* This grades Fill In Blank questions. (see SAK-1685)
* There will be two valid cases for scoring when there are multiple fill
* in blanks in a question:
* Case 1- There are different sets of answers (a set can contain one or more
* item) for each blank (e.g. The {dog|coyote|wolf} howls and the {lion|cougar}
* roars.) In this case each blank is tested for correctness independently.
* Case 2-There is the same set of answers for each blank: e.g. The flag of the US
* is {red|white|blue},{red|white|blue}, and {red|white|blue}.
* These are the only two valid types of questions. When authoring, it is an
* ERROR to include:
* (1) a mixture of independent answer and common answer blanks
* (e.g. The {dog|coyote|wolf} howls at the {red|white|blue}, {red|white|blue},
* and {red|white|blue} flag.)
* (2) more than one set of blanks with a common answer ((e.g. The US flag
* is {red|white|blue}, {red|white|blue}, and {red|white|blue} and the Italian
* flag is {red|white|greem}, {red|white|greem}, and {red|white|greem}.)
* These two invalid questions specifications should be authored as two
* separate questions.
Here are the definition and 12 cases I came up with (lydia, 01/2006):
single answers : roses are {red} and vilets are {blue}
multiple answers : {dogs|cats} have 4 legs
multiple answers , mutually exclusive, all answers must be identical, can be in diff. orders : US flag has {red|blue|white} and {red |white|blue} and {blue|red|white} colors
multiple answers , mutually non-exclusive : {dogs|cats} have 4 legs and {dogs|cats} can be pets.
wildcard uses * to mean one of more characters
-. wildcard single answer, case sensitive
-. wildcard single answer, case insensitive
-. single answer, no wildcard , case sensitive
-. single answer, no wildcard , case insensitive
-. multiple answer, mutually non-exclusive, no wildcard , case sensitive
-. multiple answer, mutually non-exclusive, no wildcard , case in sensitive
-. multiple answer, mutually non-exclusive, wildcard , case sensitive
-. multiple answer, mutually non-exclusive, wildcard , case insensitive
-. multiple answer, mutually exclusive, no wildcard , case sensitive
-. multiple answer, mutually exclusive, no wildcard , case in sensitive
-. multiple answer, mutually exclusive, wildcard , case sensitive
-. multiple answer, mutually exclusive, wildcard , case insensitive
*/
public double getFIBScore(ItemGradingData data, Map fibmap, ItemDataIfc itemdata, Map publishedAnswerHash)
{
String studentanswer = "";
boolean matchresult = false;
double totalScore = (double) 0;
data.setIsCorrect(Boolean.FALSE);
if (data.getPublishedAnswerId() == null) {
return totalScore;
}
AnswerIfc answerIfc = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
if (answerIfc == null) {
return totalScore;
}
String answertext = answerIfc.getText();
Long itemId = itemdata.getItemId();
String casesensitive = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.CASE_SENSITIVE_FOR_FIB);
String mutuallyexclusive = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.MUTUALLY_EXCLUSIVE_FOR_FIB);
String ignorespaces = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.IGNORE_SPACES_FOR_FIB);
//Set answerSet = new HashSet();
if (answertext != null)
{
StringTokenizer st = new StringTokenizer(answertext, "|");
while (st.hasMoreTokens())
{
String answer = st.nextToken().trim();
boolean ignoreSpaces = "true".equalsIgnoreCase(ignorespaces);
if ("true".equalsIgnoreCase(casesensitive)) {
if (data.getAnswerText() != null){
studentanswer= data.getAnswerText().trim();
matchresult = fibmatch(answer, studentanswer, true, ignoreSpaces);
}
} // if case sensitive
else {
// case insensitive , if casesensitive is false, or null, or "".
if (data.getAnswerText() != null){
studentanswer= data.getAnswerText().trim();
matchresult = fibmatch(answer, studentanswer, false, ignoreSpaces);
}
} // else , case insensitive
if (matchresult){
boolean alreadyused=false;
// add check for mutual exclusive
if ("true".equalsIgnoreCase(mutuallyexclusive))
{
// check if answers are already used.
Set answer_used_sofar = (HashSet) fibmap.get(itemId);
if ((answer_used_sofar!=null) && ( answer_used_sofar.contains(studentanswer.toLowerCase()))){
// already used, so it's a wrong answer for mutually exclusive questions
alreadyused=true;
}
else {
// not used, it's a good answer, now add this to the already_used list.
// we only store lowercase strings in the fibmap.
if (answer_used_sofar==null) {
answer_used_sofar = new HashSet();
}
answer_used_sofar.add(studentanswer.toLowerCase());
fibmap.put(itemId, answer_used_sofar);
}
}
if (!alreadyused) {
totalScore += ((AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId())).getScore();
data.setIsCorrect(Boolean.TRUE);
}
// SAK-3005: quit if answer is correct, e.g. if you answered A for {a|A}, you already scored
break;
}
}
}
return totalScore;
}
public boolean getFIBResult(ItemGradingData data, Map fibmap, ItemDataIfc itemdata, Map publishedAnswerHash)
{
// this method is similiar to getFIBScore(), except it returns true/false for the answer, not scores.
// may be able to refactor code out to be reused, but totalscores for mutually exclusive case is a bit tricky.
String studentanswer = "";
boolean matchresult = false;
if (data.getPublishedAnswerId() == null) {
return matchresult;
}
AnswerIfc answerIfc = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
if (answerIfc == null) {
return matchresult;
}
String answertext = answerIfc.getText();
Long itemId = itemdata.getItemId();
String casesensitive = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.CASE_SENSITIVE_FOR_FIB);
String mutuallyexclusive = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.MUTUALLY_EXCLUSIVE_FOR_FIB);
String ignorespaces = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.IGNORE_SPACES_FOR_FIB);
//Set answerSet = new HashSet();
if (answertext != null)
{
StringTokenizer st = new StringTokenizer(answertext, "|");
while (st.hasMoreTokens())
{
String answer = st.nextToken().trim();
boolean ignoreSpaces = "true".equalsIgnoreCase(ignorespaces);
if ("true".equalsIgnoreCase(casesensitive)) {
if (data.getAnswerText() != null){
studentanswer= data.getAnswerText().trim();
matchresult = fibmatch(answer, studentanswer, true, ignoreSpaces);
}
} // if case sensitive
else {
// case insensitive , if casesensitive is false, or null, or "".
if (data.getAnswerText() != null){
studentanswer= data.getAnswerText().trim();
matchresult = fibmatch(answer, studentanswer, false, ignoreSpaces);
}
} // else , case insensitive
if (matchresult){
boolean alreadyused=false;
// add check for mutual exclusive
if ("true".equalsIgnoreCase(mutuallyexclusive))
{
// check if answers are already used.
Set answer_used_sofar = (HashSet) fibmap.get(itemId);
if ((answer_used_sofar!=null) && ( answer_used_sofar.contains(studentanswer.toLowerCase()))){
// already used, so it's a wrong answer for mutually exclusive questions
alreadyused=true;
}
else {
// not used, it's a good answer, now add this to the already_used list.
// we only store lowercase strings in the fibmap.
if (answer_used_sofar==null) {
answer_used_sofar = new HashSet();
}
answer_used_sofar.add(studentanswer.toLowerCase());
fibmap.put(itemId, answer_used_sofar);
}
}
if (alreadyused) {
matchresult = false;
}
break;
}
}
}
return matchresult;
}
public double getFINScore(ItemGradingData data, ItemDataIfc itemdata, Map publishedAnswerHash) throws FinFormatException
{
data.setIsCorrect(Boolean.FALSE);
double totalScore = (double) 0;
boolean matchresult = getFINResult(data, itemdata, publishedAnswerHash);
if (matchresult){
totalScore += ((AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId())).getScore();
data.setIsCorrect(Boolean.TRUE);
}
return totalScore;
}
public boolean getFINResult (ItemGradingData data, ItemDataIfc itemdata, Map publishedAnswerHash) throws FinFormatException
{
String studentanswer = "";
boolean range;
boolean matchresult = false;
ComplexFormat complexFormat = new ComplexFormat();
Complex answerComplex = null;
Complex studentAnswerComplex;
BigDecimal answerNum = null, answer1Num = null, answer2Num = null, studentAnswerNum;
if (data.getPublishedAnswerId() == null) {
return false;
}
AnswerIfc answerIfc = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
if (answerIfc == null) {
return matchresult;
}
String answertext = answerIfc.getText();
if (answertext != null)
{
StringTokenizer st = new StringTokenizer(answertext, "|");
range = false;
if (st.countTokens() > 1) {
range = true;
}
String studentAnswerText = null;
if (data.getAnswerText() != null) {
studentAnswerText = data.getAnswerText().replaceAll("\\s+", "").replace(',','.'); // in Spain, comma is used as a decimal point
//The UI syntax expects a format with curly braces for complex numbers, remove them for the Commons Math library.
if(StringUtils.contains(studentAnswerText, "{") && StringUtils.contains(studentAnswerText, "}")){
studentAnswerText = StringUtils.replace(studentAnswerText, "{", "");
studentAnswerText = StringUtils.replace(studentAnswerText, "}", "");
}
}
if (range) {
String answer1 = st.nextToken().trim();
String answer2 = st.nextToken().trim();
if (answer1 != null){
answer1 = answer1.trim().replace(',','.'); // in Spain, comma is used as a decimal point
}
if (answer2 != null){
answer2 = answer2.trim().replace(',','.'); // in Spain, comma is used as a decimal point
}
try {
answer1Num = new BigDecimal(answer1);
answer2Num = new BigDecimal(answer2);
} catch (Exception e) {
log.debug("Number is not BigDecimal: " + answer1 + " or " + answer2);
}
Map map = validate(studentAnswerText);
studentAnswerNum = (BigDecimal) map.get(ANSWER_TYPE_REAL);
matchresult = (answer1Num != null && answer2Num != null && studentAnswerNum != null &&
(answer1Num.compareTo(studentAnswerNum) <= 0) && (answer2Num.compareTo(studentAnswerNum) >= 0));
}
else { // not range
String answer = st.nextToken().trim();
if (answer != null){
answer = answer.replaceAll("\\s+", "").replace(',','.'); // in Spain, comma is used as a decimal point
//The UI syntax expects a format with curly braces, remove them for the Commons Math library.
if(StringUtils.contains(answer, "{") && StringUtils.contains(answer, "}")){
answer = StringUtils.replace(answer, "{", "");
answer = StringUtils.replace(answer, "}", "");
}
}
try {
answerNum = new BigDecimal(answer);
} catch(NumberFormatException ex) {
log.debug("Number is not BigDecimal: " + answer);
}
try {
answerComplex = complexFormat.parse(answer);
} catch(MathParseException ex) {
log.debug("Number is not Complex: " + answer);
}
if (data.getAnswerText() != null) {
Map map = validate(studentAnswerText);
if (answerNum != null) {
studentAnswerNum = (BigDecimal) map.get(ANSWER_TYPE_REAL);
matchresult = (studentAnswerNum != null && answerNum.compareTo(studentAnswerNum) == 0);
}
else if (answerComplex != null) {
studentAnswerComplex = (Complex) map.get(ANSWER_TYPE_COMPLEX);
matchresult = (studentAnswerComplex != null && answerComplex.equals(studentAnswerComplex));
}
}
}
}
return matchresult;
}
public double getImageMapScore(ItemGradingData data, ItemDataIfc itemdata, Map publishedItemTextHash, Map publishedAnswerHash)
{
// Final score must be...
// IF NOT PARTIALCREDIT THEN 0 or total
// IF PARTIALCREDIT EACH PART ADDED.
data.setIsCorrect(Boolean.FALSE);
double totalScore;
Iterator iter = publishedAnswerHash.keySet().iterator();
int answerNumber = 0;
while (iter.hasNext()){
Long answerId = Long.valueOf(iter.next().toString());
AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(answerId);
if (answer.getItem().getItemId().equals(data.getPublishedItemId()))
answerNumber=answerNumber+1;
}
double answerScore=itemdata.getScore();
if (answerNumber!=0){
answerScore=answerScore/answerNumber;
}
ItemTextIfc itemTextIfc = (ItemTextIfc) publishedItemTextHash.get(data.getPublishedItemTextId());
List answerArray = (List) itemTextIfc.getAnswerArray();
AnswerIfc answerIfc= (AnswerIfc) answerArray.get(0);
try{
String area = answerIfc.getText();
Integer areax1=Integer.valueOf(area.substring(area.indexOf("\"x1\":")+5,area.indexOf(",", area.indexOf("\"x1\":"))));
Integer areay1=Integer.valueOf(area.substring(area.indexOf("\"y1\":")+5,area.indexOf(",", area.indexOf("\"y1\":"))));
Integer areax2=Integer.valueOf(area.substring(area.indexOf("\"x2\":")+5,area.indexOf(",", area.indexOf("\"x2\":"))));
Integer areay2=Integer.valueOf(area.substring(area.indexOf("\"y2\":")+5,area.indexOf("}", area.indexOf("\"y2\":"))));
String point = data.getAnswerText();
Integer pointx=Integer.valueOf(point.substring(point.indexOf("\"x\":")+4,point.indexOf(",", point.indexOf("\"x\":"))));
Integer pointy=Integer.valueOf(point.substring(point.indexOf("\"y\":")+4,point.indexOf("}", point.indexOf("\"y\":"))));
if (((pointx>=areax1)&&(pointx<=areax2))&&((pointy>=areay1)&&(pointy<=areay2))) {
totalScore=answerScore;
data.setIsCorrect(Boolean.TRUE);
}else{
totalScore=0;
}
}catch(Exception ex){
totalScore=0;
}
return totalScore;
}
/**
* Validate a students numeric answer
* @param value answer to validate
* @return a Map containing either Real or Complex answer keyed by {@link #ANSWER_TYPE_REAL} or {@link #ANSWER_TYPE_COMPLEX}
*/
public Map validate(String value) {
Map map = new HashMap();
if (StringUtils.isEmpty(value)) {
return map;
}
String trimmedValue = value.trim();
boolean isComplex = true;
boolean isRealNumber = true;
BigDecimal studentAnswerReal = null;
try {
studentAnswerReal = new BigDecimal(trimmedValue);
} catch (Exception e) {
isRealNumber = false;
}
// Test for complex number only if it is not a BigDecimal
Complex studentAnswerComplex = null;
if (!isRealNumber) {
try {
DecimalFormat df = (DecimalFormat)NumberFormat.getNumberInstance(Locale.US);
df.setGroupingUsed(false);
// Numerical format ###.## (decimal symbol is the point)
ComplexFormat complexFormat = new ComplexFormat(df);
studentAnswerComplex = complexFormat.parse(trimmedValue);
// Only checks for complex numbers, not real numbers
if (studentAnswerComplex.getImaginary() == 0) {
isComplex = false;
}
} catch (Exception e) {
isComplex = false;
}
}
Boolean isValid = isComplex || isRealNumber;
if (!isValid) {
throw new FinFormatException("Not a valid FIN Input. studentanswer=" + trimmedValue);
}
if (isRealNumber) {
map.put(ANSWER_TYPE_REAL, studentAnswerReal);
}
else if (isComplex) {
map.put(ANSWER_TYPE_COMPLEX, studentAnswerComplex);
}
return map;
}
/**
* EMI score processing
*
*/
private double getEMIScore(ItemGradingData itemGrading, Long itemId,
Map totalItems, Map>> emiScoresMap,
Map publishedItemTextHash, Map publishedAnswerHash) {
log.debug("getEMIScore( " + itemGrading +", " + itemId);
double autoScore;
if (!totalItems.containsKey(itemId)) {
totalItems.put(itemId, new HashMap());
emiScoresMap.put(itemId, new HashMap<>());
}
autoScore = getAnswerScore(itemGrading, publishedAnswerHash);
AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(itemGrading
.getPublishedAnswerId());
if (answer == null) {
//its possible we have an orphaned object ...
log.warn("could not find answer: " + itemGrading
.getPublishedAnswerId() + ", for item " + itemGrading.getItemGradingId());
return 0.0;
}
Long itemTextId = itemGrading.getPublishedItemTextId();
// update the fibEmiAnswersMap so we can keep track
// of how many answers were given
Map> emiItemScoresMap = emiScoresMap.get(itemId);
// place the answer scores in a sorted set.
// so now we can mark the correct ones and discount the extra incorrect
// ones.
Set scores;
if (emiItemScoresMap.containsKey(itemTextId)) {
scores = emiItemScoresMap.get(itemTextId);
} else {
scores = new TreeSet<>();
emiItemScoresMap.put(itemTextId, scores);
}
scores.add(new EMIScore(itemId, itemTextId, itemGrading
.getPublishedAnswerId(), answer.getIsCorrect(), autoScore));
ItemTextIfc itemText = (ItemTextIfc) publishedItemTextHash.get(itemTextId);
int numberCorrectAnswers = itemText.getEmiCorrectOptionLabels()
.length();
Integer requiredCount = itemText.getRequiredOptionsCount();
// re-calculate the scores over for the whole item
autoScore = 0.0;
int c = 0;
for (EMIScore s : scores) {
c++;
s.effectiveScore = 0.0;
if (c <= numberCorrectAnswers && c <= requiredCount) {
// if correct and in count then add score
s.effectiveScore = s.correct ? s.score : 0.0;
} else if (c > numberCorrectAnswers) {
// if incorrect and over count add discount
s.effectiveScore = !s.correct ? s.score : 0.0;
}
if (autoScore + s.effectiveScore < 0.0) {
// the current item tipped it to negative,
// we cannot do this, so add zero
s.effectiveScore = 0.0;
}
autoScore += s.effectiveScore;
}
// override score
if (itemGrading.getOverrideScore() != null)
autoScore += itemGrading.getOverrideScore();
Map totalItemTextScores = (Map) totalItems.get(itemId);
totalItemTextScores.put(itemTextId, autoScore);
return autoScore;
}
/**
* CALCULATED_QUESTION
* Returns a double score value for the ItemGrading element being scored for a Calculated Question
*
* @param calcQuestionAnswerSequence the order of answers in the list
* @return score for the item.
*/
public double getCalcQScore(ItemGradingData data, ItemDataIfc itemdata, Map calculatedAnswersMap, int calcQuestionAnswerSequence)
{
double totalScore = (double) 0;
if (data.getAnswerText() == null) return totalScore; // zero for blank
if (!calculatedAnswersMap.containsKey(calcQuestionAnswerSequence)) {
return totalScore;
}
// this variable should look something like this "42.1|2,2"
String allAnswerText = calculatedAnswersMap.get(calcQuestionAnswerSequence);
// NOTE: this correctAnswer will already have been trimmed to the appropriate number of decimals
BigDecimal correctAnswer = new BigDecimal(getAnswerExpression(allAnswerText));
// Determine if the acceptable variance is a constant or a % of the answer
String varianceString = allAnswerText.substring(allAnswerText.indexOf("|")+1, allAnswerText.indexOf(","));
BigDecimal acceptableVariance;
if (varianceString.contains("%")){
double percentage = Double.valueOf(varianceString.substring(0, varianceString.indexOf("%")));
acceptableVariance = correctAnswer.multiply( new BigDecimal(percentage / 100) );
}
else {
acceptableVariance = new BigDecimal(varianceString);
}
String userAnswerString = data.getAnswerText().replaceAll(",|[\\s]+", "");
BigDecimal userAnswer;
try {
userAnswer = new BigDecimal(userAnswerString);
} catch(NumberFormatException nfe) {
return totalScore; // zero because it's not even a number!
}
// this compares the correctAnswer against the userAnsewr
BigDecimal answerDiff = (correctAnswer.subtract(userAnswer));
boolean closeEnough = (answerDiff.abs().compareTo(acceptableVariance.abs()) <= 0);
if (closeEnough){
totalScore += itemdata.getScore();
}
return totalScore;
}
public boolean getCalcQResult(ItemGradingData data, ItemDataIfc itemdata, Map calculatedAnswersMap, int calcQuestionAnswerSequence)
{
boolean result = false;
if (data.getAnswerText() == null) return result;
if (!calculatedAnswersMap.containsKey(calcQuestionAnswerSequence)) {
return result;
}
// this variable should look something like this "42.1|2,2"
String allAnswerText = calculatedAnswersMap.get(calcQuestionAnswerSequence);
// NOTE: this correctAnswer will already have been trimmed to the appropriate number of decimals
BigDecimal correctAnswer = new BigDecimal(getAnswerExpression(allAnswerText));
// Determine if the acceptable variance is a constant or a % of the answer
String varianceString = allAnswerText.substring(allAnswerText.indexOf("|")+1, allAnswerText.indexOf(","));
BigDecimal acceptableVariance;
if (varianceString.contains("%")){
double percentage = Double.valueOf(varianceString.substring(0, varianceString.indexOf("%")));
acceptableVariance = correctAnswer.multiply( new BigDecimal(percentage / 100) );
}
else {
acceptableVariance = new BigDecimal(varianceString);
}
String userAnswerString = data.getAnswerText().replaceAll(",|[\\s]+", "");
BigDecimal userAnswer;
try {
userAnswer = new BigDecimal(userAnswerString);
} catch(NumberFormatException nfe) {
return result;
}
// this compares the correctAnswer against the userAnsewr
BigDecimal answerDiff = (correctAnswer.subtract(userAnswer));
boolean closeEnough = (answerDiff.abs().compareTo(acceptableVariance.abs()) <= 0);
if (closeEnough){
result = true;
}
return result;
}
public double getTotalCorrectScore(ItemGradingData data, Map publishedAnswerHash)
{
AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
if (answer == null || answer.getScore() == null)
return (double) 0;
return answer.getScore();
}
private void setIsLate(AssessmentGradingData data, PublishedAssessmentIfc pub){
Boolean isLate = Boolean.FALSE;
AssessmentAccessControlIfc a = pub.getAssessmentAccessControl();
if (a.getDueDate() != null && a.getDueDate().before(new Date())) {
isLate = Boolean.TRUE;
}
if (isLate) {
ExtendedTimeDeliveryService assessmentExtended = new ExtendedTimeDeliveryService((PublishedAssessmentFacade) pub, data.getAgentId());
if (assessmentExtended.hasExtendedTime() && assessmentExtended.getDueDate() != null && assessmentExtended.getDueDate().after(new Date())) {
isLate = Boolean.FALSE;
}
}
data.setIsLate(isLate);
if (data.getForGrade())
data.setStatus(1);
data.setTotalOverrideScore(Double.valueOf(0));
}
public void deleteAll(Collection c)
{
try {
PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().deleteAll(c);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
/* Note:
* assessmentGrading contains set of itemGrading that are not saved in the DB yet
*/
public void updateAssessmentGradingScore(AssessmentGradingData adata, PublishedAssessmentIfc pub, String newComment, String oldComment){
try {
Set itemGradingSet = adata.getItemGradingSet();
Iterator iter = itemGradingSet.iterator();
double totalAutoScore = 0;
double totalOverrideScore = adata.getTotalOverrideScore();
while (iter.hasNext()){
ItemGradingData i = (ItemGradingData)iter.next();
if (i.getAutoScore()!=null)
totalAutoScore += i.getAutoScore();
}
double oldAutoScore = adata.getTotalAutoScore();
double scoreDifference = totalAutoScore - oldAutoScore;
adata.setTotalAutoScore(totalAutoScore);
if (Double.compare((totalAutoScore+totalOverrideScore),Double.valueOf("0"))<0){
adata.setFinalScore(Double.valueOf("0"));
}else{
adata.setFinalScore(totalAutoScore+totalOverrideScore);
}
saveOrUpdateAssessmentGrading(adata);
if (scoreDifference != 0 || !newComment.equals(oldComment)){
notifyGradebookByScoringType(adata, pub);
}
} catch (GradebookServiceException ge) {
log.error(ge.getMessage(), ge);
throw ge;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
public void saveOrUpdateAll(Collection c)
{
try {
PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().saveOrUpdateAll(c);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public PublishedAssessmentIfc getPublishedAssessmentByAssessmentGradingId(String id){
PublishedAssessmentIfc pub = null;
try {
pub = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getPublishedAssessmentByAssessmentGradingId(Long.valueOf(id));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return pub;
}
public PublishedAssessmentIfc getPublishedAssessmentByPublishedItemId(String publishedItemId){
PublishedAssessmentIfc pub = null;
try {
pub = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getPublishedAssessmentByPublishedItemId(Long.valueOf(publishedItemId));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return pub;
}
public List getLastItemGradingDataPosition(Long assessmentGradingId, String agentId) {
List results = null;
try {
results = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getLastItemGradingDataPosition(assessmentGradingId, agentId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public List getItemGradingIds(Long assessmentGradingId) {
List results = null;
try {
results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getItemGradingIds(assessmentGradingId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public List getPublishedItemIds(Long assessmentGradingId) {
List results = null;
try {
results = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getPublishedItemIds(assessmentGradingId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public Set getItemSet(Long publishedAssessmentId, Long sectionId) {
Set results = null;
try {
results = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getItemSet(publishedAssessmentId, sectionId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public Long getTypeId(Long itemGradingId) {
Long typeId = null;
try {
typeId = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getTypeId(itemGradingId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return typeId;
}
public boolean fibmatch(final String rawAnswer, final String rawInput, final boolean casesensitive, final boolean ignorespaces) {
log.debug("Fill in the blank match called with inputs:\nAnswer={}\nInput={}\nCase Sensitive={}\nIgnore Spaces={}", rawAnswer, rawInput, casesensitive, ignorespaces);
try {
// User on Mac will input ü instead of ü
String answer = StringEscapeUtils.unescapeHtml4(rawAnswer);
String input = StringEscapeUtils.unescapeHtml4(rawInput);
// User on Mac will use "smart" curly apostrophes by default!!
answer = answer.replaceAll("[\u2018\u2019]", "'");
input = input.replaceAll("[\u2018\u2019]", "'");
// User on Mac will use “smart” quotes by default!
answer = answer.replaceAll("[\u201C\u201D]", "\"");
input = input.replaceAll("[\u201C\u201D]", "\"");
// Always trim trailing spaces
answer = answer.trim();
input = input.trim();
// Trim interior space including non-breaking spaces if instructor selects option
if (ignorespaces) {
answer = answer.replaceAll("\\p{javaSpaceChar}", "");
input = input.replaceAll("\\p{javaSpaceChar}", "");
}
StringBuilder regex_quotebuf = new StringBuilder();
String[] oneblank = answer.replaceAll("\\*", "|*|").split("\\|");
for (String str : oneblank) {
if ("*".equals(str)) {
regex_quotebuf.append(".+");
}
else {
regex_quotebuf.append(Pattern.quote(str));
}
}
String regex_quote = regex_quotebuf.toString();
Pattern p;
if (casesensitive){
p = Pattern.compile(regex_quote );
}
else {
p = Pattern.compile(regex_quote,Pattern.CASE_INSENSITIVE|Pattern.UNICODE_CASE);
}
Matcher m = p.matcher(input);
boolean result = m.matches();
return result;
}
catch (Exception e){
log.warn("Fill in the blank match failed returning false, {}", e.toString());
return false;
}
}
public List getAllAssessmentGradingByAgentId(Long publishedAssessmentId, String agentIdString) {
List results = null;
try {
results = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getAllAssessmentGradingByAgentId(publishedAssessmentId, agentIdString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public List getAllItemGradingDataForItemInGrading(Long assesmentGradingId, Long publihsedItemId) {
List results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getAllItemGradingDataForItemInGrading(assesmentGradingId, publihsedItemId);
return results;
}
public Map> getSiteSubmissionCountHash(String siteId) {
Map> results = new HashMap<>();
try {
results = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getSiteSubmissionCountHash(siteId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public Map> getSiteInProgressCountHash(final String siteId) {
Map> results = new HashMap<>();
try {
results = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getSiteInProgressCountHash(siteId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public int getActualNumberRetake(Long publishedAssessmentId, String agentIdString) {
int actualNumberReatke = 0;
try {
actualNumberReatke = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getActualNumberRetake(publishedAssessmentId, agentIdString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return actualNumberReatke;
}
public Map getActualNumberRetakeHash(String agentIdString) {
Map actualNumberRetakeHash = new HashMap<>();
try {
actualNumberRetakeHash = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getActualNumberRetakeHash(agentIdString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return actualNumberRetakeHash;
}
public Map> getSiteActualNumberRetakeHash(String siteIdString) {
Map> numberRetakeHash = new HashMap<>();
try {
numberRetakeHash = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getSiteActualNumberRetakeHash(siteIdString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return numberRetakeHash;
}
public List getStudentGradingSummaryData(Long publishedAssessmentId, String agentIdString) {
List results = null;
try {
results = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getStudentGradingSummaryData(publishedAssessmentId, agentIdString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return results;
}
public int getNumberRetake(Long publishedAssessmentId, String agentIdString) {
int numberRetake = 0;
try {
numberRetake = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getNumberRetake(publishedAssessmentId, agentIdString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return numberRetake;
}
public Map getNumberRetakeHash(String agentIdString) {
Map numberRetakeHash = new HashMap<>();
try {
numberRetakeHash = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getNumberRetakeHash(agentIdString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return numberRetakeHash;
}
public Map> getSiteNumberRetakeHash(String siteIdString) {
Map> siteActualNumberRetakeList = new HashMap();
try {
siteActualNumberRetakeList = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getSiteNumberRetakeHash(siteIdString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return siteActualNumberRetakeList;
}
public void saveStudentGradingSummaryData(StudentGradingSummaryIfc studentGradingSummaryData) {
try {
PersistenceService.getInstance().getAssessmentGradingFacadeQueries().saveStudentGradingSummaryData(studentGradingSummaryData);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public int getLateSubmissionsNumberByAgentId(Long publishedAssessmentId, String agentIdString, Date dueDate) {
int numberRetake = 0;
try {
numberRetake = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getLateSubmissionsNumberByAgentId(publishedAssessmentId, agentIdString, dueDate);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return numberRetake;
}
/**
*
* @param publishedAssessmentId
* @param anonymous
* @param audioMessage
* @param fileUploadMessage
* @param noSubmissionMessage
* @param showPartAndTotalScoreSpreadsheetColumns
* @param poolString
* @param partString
* @param questionString
* @param textString
* @param rationaleString
* @param itemGradingCommentsString
* @param useridMap
* @param responseCommentString
* @return a list of responses or null if there are none
*/
public List getExportResponsesData(String publishedAssessmentId, boolean anonymous, String audioMessage, String fileUploadMessage, String noSubmissionMessage, boolean showPartAndTotalScoreSpreadsheetColumns, String poolString, String partString, String questionString, String textString, String rationaleString, String itemGradingCommentsString, Map useridMap, String responseCommentString) {
List list = null;
try {
list = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getExportResponsesData(publishedAssessmentId, anonymous,audioMessage, fileUploadMessage, noSubmissionMessage, showPartAndTotalScoreSpreadsheetColumns, poolString, partString, questionString, textString, rationaleString, itemGradingCommentsString, useridMap, responseCommentString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return list;
}
private void removeUnsubmittedAssessmentGradingData(AssessmentGradingData data){
try {
PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().removeUnsubmittedAssessmentGradingData(data);
} catch (Exception e) {
log.error("Exception thrown from removeUnsubmittedAssessmentGradingData" + e.getMessage());
}
}
public void removeAssessmentGradingData(AssessmentGradingData data) {
try {
PersistenceService.getInstance().getAssessmentGradingFacadeQueries().removeAssessmentGradingData(data);
} catch (Exception e) {
log.error("Exception thrown from removeAssessmentGradingData", e);
}
}
public boolean getHasGradingData(Long publishedAssessmentId) {
boolean hasGradingData = false;
try {
hasGradingData = PersistenceService.getInstance().
getAssessmentGradingFacadeQueries().getHasGradingData(publishedAssessmentId);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return hasGradingData;
}
/**
* CALCULATED_QUESTION
* @param itemGrading
* @param item
* @return map of calc answers
*/
private Map getCalculatedAnswersMap(ItemGradingData itemGrading, ItemDataIfc item, int calcQuestionAnswerSequence ) {
// return value from extractCalcQAnswersArray is not used, calculatedAnswersMap is populated by this call
if (calcQuestionAnswerSequence == 1) {
extractCalcQAnswersArray(answersMap, item, itemGrading.getAssessmentGradingId(), itemGrading.getAgentId());
}
return answersMap;
}
/**
* extractCalculations() is a utility function for Calculated Questions. It takes
* one parameter, which is a block of text, and looks for any calculations
* that are encoded in the text. A calculations is enclosed in [[ ]].
* For example, if the passed parameter is {a} + {b} = {{c}}, [[{a}+{b}]]
,
* the resulting list would contain one entry: a string of "{a}+{b}"
*
Formulas must contain at least one variable OR parens OR calculation symbol (*-+/)
* @param text contents to be searched
* @return a list of matching calculations. If no calculations are found, the
* list will be empty.
*/
public List extractCalculations(String text) {
text = text.replaceAll("\\[\\[", "" + CALCULATION_AUX_OPEN + CALCULATION_AUX_OPEN ).replaceAll("\\]\\](?!\\])", "" + CALCULATION_AUX_CLOSE + CALCULATION_AUX_CLOSE);
List calculations = extractCalculatedQuestionKeyFromItemText(text, CALCQ_CALCULATION_PATTERN);
for (Iterator iterator = calculations.iterator(); iterator.hasNext();) {
String calc = iterator.next();
if (!StringUtils.containsAny(calc, "{}()+-*/")) {
iterator.remove();
}
}
return calculations;
}
/**
* extractFormulas() is a utility function for Calculated Questions. It takes
* one parameter, which is a block of text, and looks for any formula names
* that are encoded in the text. A formula name is enclosed in {{ }}. The
* formula itself is encoded elsewhere.
* For example, if the passed parameter is {a} + {b} = {{c}}
,
* the resulting list would contain one entry: a string of "c"
*
Formulas must begin with an alpha, but subsequent character can be
* alpha-numeric
* @param text contents to be searched
* @return a list of matching formula names. If no formulas are found, the
* list will be empty.
*/
public List extractFormulas(String text) {
return extractCalculatedQuestionKeyFromItemText(text, CALCQ_FORMULA_PATTERN);
}
/**
* extractVariables() is a utility function for Calculated Questions. It
* takes one parameter, which is a block of text, and looks for any variable
* names that are encoded in the text. A variable name is enclosed in { }.
* The values of the variable are encoded elsewhere.
* For example, if the passed parameter is {a} + {b} = {{c}}
,
* the resulting list would contain two entries: strings of "a" and "b"
*
Variables must begin with an alpha, but subsequent character can be
* alpha-numeric.
*
Note - a formula, encoded as {{ }}, will not be mistaken for a variable.
* @param text content to be searched
* @return a list of matching variable names. If no variables are found, the
* list will be empty
*/
public List extractVariables(String text) {
return extractCalculatedQuestionKeyFromItemText(text, CALCQ_ANSWER_PATTERN);
}
/**
* extractCalculatedQuestionKeyFromItemText() is a utility function for Calculated Questions. It
* takes a block of item text, and uses a pattern to looks for keys
* that are encoded in the text.
* @param itemText content to be searched
* @param identifierPattern pattern to use to do the search
* @return a list of matching key values OR empty if none are found
*/
private List extractCalculatedQuestionKeyFromItemText(String itemText, Pattern identifierPattern) {
List keys = new ArrayList<>();
if (itemText != null && itemText.trim().length() > 0) {
Matcher keyMatcher = identifierPattern.matcher(itemText);
while (keyMatcher.find()) {
String match = keyMatcher.group(1);
keys.add(match);
/*
// first character before matching group
int start = keyMatcher.start(1) - 2;
// first character after matching group
int end = keyMatcher.end(1) + 1; // first character after the matching group
// if matching group is wrapped by {}, it's not what we are looking for (like another key or just some text)
if (start < 0 || end >= itemText.length() || itemText.charAt(start) != '{' || itemText.charAt(end) != '}') {
keys.add(match);
}*/
}
}
return keys;
}
/**
* CALCULATED_QUESTION
* @param item the item which contains the formula
* @param formulaName the name of the formula
* @return the actual formula that matches this formula name OR "" (empty string) if it is not found
*/
private String replaceFormulaNameWithFormula(ItemDataIfc item, String formulaName) {
String result = "";
@SuppressWarnings("unchecked")
List items = item.getItemTextArray();
for (ItemTextIfc itemText : items) {
if (itemText.getText().equals(formulaName)) {
@SuppressWarnings("unchecked")
List answers = itemText.getAnswerArray();
for (AnswerIfc answer : answers) {
if (itemText.getSequence().equals(answer.getSequence())) {
result = answer.getText();
break;
}
}
}
}
return result;
}
/**
* CALCULATED_QUESTION
* Takes the instructions and breaks it into segments, based on the location
* of formula names. One formula would give two segments, two formulas gives
* three segments, etc.
* Note - in this context, it would probably be easier if any variable value
* substitutions have occurred before the breakup is done; otherwise,
* each segment will need to have substitutions done.
* @param instructions string to be broken up
* @return the original string, broken up based on the formula name delimiters
*/
protected List extractInstructionSegments(String instructions) {
List segments = new ArrayList<>();
if (instructions != null && instructions.length() > 0) {
String[] results = CALCQ_FORMULA_SPLIT_PATTERN.split(instructions); // only works because all variables and calculations are already replaced
segments.addAll(Arrays.asList(results));
if (segments.size() == 1) {
// add in the trailing segment
segments.add("");
}
/*
final String FUNCTION_BEGIN = "{{";
final String FUNCTION_END = "}}";
while (instructions.indexOf(FUNCTION_BEGIN) > -1 && instructions.indexOf(FUNCTION_END) > -1) {
String segment = instructions.substring(0, instructions.indexOf(FUNCTION_BEGIN));
instructions = instructions.substring(instructions.indexOf(FUNCTION_END) + FUNCTION_END.length());
segments.add(segment);
}
segments.add(instructions);
*/
}
return segments;
}
/**
* CALCULATED_QUESTION
* toScientificNotation() Takes a string representation of a number and returns
* a string representation of that number, in scientific notation.
* Numbers like 100, 0.01 will not be formatted (see values of MAX_THRESHOLD and MIN_THRESHOLD)
* @param numberStr
* @param decimalPlaces
* @return processed number string
*/
public String toScientificNotation(String numberStr,int decimalPlaces){
BigDecimal bdx = new BigDecimal(numberStr);
bdx.setScale(decimalPlaces,RoundingMode.HALF_UP);
NumberFormat formatter;
if ((bdx.abs().compareTo(DEFAULT_MAX_THRESHOLD) >= 0 || bdx.abs().compareTo(DEFAULT_MIN_THRESHOLD) <= 0
|| numberStr.contains("e") || numberStr.contains("E") )
&& bdx.doubleValue() != 0) {
formatter = new DecimalFormat(FORMAT_MASK);
} else {
formatter = new DecimalFormat("0");
}
formatter.setRoundingMode(RoundingMode.HALF_UP);
formatter.setMaximumFractionDigits(decimalPlaces);
String formattedNumber = formatter.format(bdx);
return formattedNumber.replace(",",".");
}
/**
* CALCULATED_QUESTION
* toScientificNotation() Takes a string representation of a number and returns
* a string representation of that number, in scientific notation.
* Numbers like 100, 0.01 will not be formatted (see values of MAX_THRESHOLD and MIN_THRESHOLD)
* @param numberStr
* @param decimalPlaces
* @param stringCalculate
* @return processed number string
*/
public String toScientificNotation(String numberStr, String stringCalculate, int decimalPlaces){
BigDecimal bdx = new BigDecimal(numberStr);
bdx.setScale(decimalPlaces,RoundingMode.HALF_UP);
NumberFormat formatter;
if ((bdx.abs().compareTo(DEFAULT_MAX_THRESHOLD) >= 0 || bdx.abs().compareTo(DEFAULT_MIN_THRESHOLD) <= 0
|| numberStr.contains("e") || numberStr.contains("E") )
&& bdx.doubleValue() != 0) {
formatter = new DecimalFormat(FORMAT_MASK);
} else {
if (!numberStr.equals(stringCalculate) && ("0".equals(stringCalculate))) {
formatter = new DecimalFormat(FORMAT_MASK);
} else {
formatter = new DecimalFormat("0");
}
}
formatter.setRoundingMode(RoundingMode.HALF_UP);
formatter.setMaximumFractionDigits(decimalPlaces);
String formattedNumber = formatter.format(bdx);
return formattedNumber.replace(",",".");
}
/**
* CALCULATED_QUESTION
* applyPrecisionToNumberString() takes a string representation of a number and returns
* a string representation of that number, rounded to the specified number of
* decimal places, including trimming decimal places if needed.
* Will also throw away the extra trailing zeros as well as removing a trailing decimal point.
* @param numberStr
* @param decimalPlaces
* @return processed number string (will never be null or empty string)
*/
public String applyPrecisionToNumberString(String numberStr, int decimalPlaces) {
// Trim off excess decimal points based on decimalPlaces value
BigDecimal bd = new BigDecimal(numberStr);
bd = bd.setScale(decimalPlaces, BigDecimal.ROUND_HALF_EVEN);
String decimal = ".";
// TODO handle localized decimal separator?
//DecimalFormatSymbols dfs = new DecimalFormatSymbols(Locale);
//char dec = dfs.getDecimalFormatSymbols().getDecimalSeparator();
String displayAnswer = bd.toString();
if (displayAnswer.length() > 2 && displayAnswer.contains(decimal)) {
if (decimalPlaces == 0) { // Remove ".0" if decimalPlaces == 0
displayAnswer = displayAnswer.replace(decimal+"0", "");
} else {
// trim away all the extra 0s from the end of the number
if (displayAnswer.endsWith("0")) {
displayAnswer = StringUtils.stripEnd(displayAnswer, "0");
}
if (displayAnswer.endsWith(decimal)) {
displayAnswer = displayAnswer.substring(0, displayAnswer.length() - 1);
}
}
}
return displayAnswer;
}
/**
* CALCULATED_QUESTION
* calculateFormulaValues() evaluates all formulas referenced in the
* instructions. For each formula name it finds, it retrieves the formula
* for the name, substitutes the randomized value for the variables,
* evaluates the formula to a real answer, and put the answer, along with
* the needed precision and decimal places in the returning Map.
* @param variables a Map of variables, The key is the
* variable name, the value is the text representation, after randomization,
* of a number in the variable's defined range.
* @param item The question itself, which is needed to provide additional
* information for called functions
* @return a Map. the Integer is simply the sequence.
* Answers are returned in the order that the formulas are found.
* The String is the result of the formula, encoded as (value)|(tolerance),(decimal places)
* @throws Exception if either the formula expression fails to pass the
* Samigo expression parser, which should never happen as this is validated
* when the question is saved, or if a divide by zero error occurs.
*/
private Map calculateFormulaValues(Map variables, ItemDataIfc item) throws Exception {
Map values = new HashMap<>();
String instructions = item.getInstruction();
List formulaNames = this.extractFormulas(instructions);
for (int i = 0; i < formulaNames.size(); i++) {
String formulaName = formulaNames.get(i);
String longFormula = replaceFormulaNameWithFormula(item, formulaName); // {a}+{b}|0.1,1
longFormula = defaultVarianceAndDecimal(longFormula); // sets defaults, in case tolerance or precision isn't set
String formula = getAnswerExpression(longFormula); // returns just the formula
String answerData = getAnswerData(longFormula); // returns just tolerance and precision
int decimalPlaces = getAnswerDecimalPlaces(answerData);
String substitutedFormula = replaceMappedVariablesWithNumbers(formula,variables);
String formulaValue = processFormulaIntoValue(substitutedFormula, decimalPlaces);
values.put(i + 1, formulaValue + answerData); // later answerData will be used for scoring
}
return values;
}
/**
* CALCULATED_QUESTION
* This is a busy method. It does three things:
*
1. It removes the answer expressions ie. {{x+y}} from the question text. This value is
* returned in the ArrayList texts. This format is necessary so that input boxes can be
* placed in the text where the {{..}}'s appear.
*
2. It will call methods to swap out the defined variables with randomly generated values
* within the ranges defined by the user.
*
3. It populates the HashMap answerList with the calculated answers in sequence. It will
* parse and calculate what each answer needs to be.
* Note: If a divide by zero occurs, we change the random values and try again. It gets limited chances to
* get valid values and then will return "infinity" as the answer.
* @param answerList is cleared and filled with sequential answers to the question
* @return ArrayList of the pieces of text to display surrounding input boxes
*/
public List extractCalcQAnswersArray(Map answerList, ItemDataIfc item, Long gradingId, String agentId) {
boolean hasErrors = true;
Map variableRangeMap = buildVariableRangeMap(item);
List instructionSegments = new ArrayList<>(0);
answerList.clear();
int attemptCount = 1;
while (hasErrors && attemptCount <= MAX_ERROR_TRIES) {
instructionSegments.clear();
Map variablesWithValues = determineRandomValuesForRanges(variableRangeMap,item.getItemId(), gradingId, agentId, attemptCount);
try {
Map evaluatedFormulas = calculateFormulaValues(variablesWithValues, item);
answerList.putAll(evaluatedFormulas);
// replace the variables in the text with values
String instructions = item.getInstruction();
instructions = replaceMappedVariablesWithNumbers(instructions, variablesWithValues);
// then replace the calculations with values (must happen AFTER the variable replacement)
try {
instructions = replaceCalculationsWithValues(instructions, 5); // what decimal precision should we use here?
// if could not process the calculation into a result then throws IllegalStateException which will be caught below and cause the numbers to regenerate
// only pull out the segments if the formulas worked
instructionSegments = extractInstructionSegments(instructions);
hasErrors = false;
} catch (SamigoExpressionError e1) {
log.warn("Samigo calculated item ({}) calculation invalid: {}", item.getItemId(), e1.get());
attemptCount++;
}
} catch (Exception e) {
// possible division by zero error
attemptCount++;
}
}
return instructionSegments;
}
/**
* CALCULATED_QUESTION
* This returns the decimal places value in the stored answer data.
* @param allAnswerText
* @return
*/
private int getAnswerDecimalPlaces(String allAnswerText) {
String answerData = getAnswerData(allAnswerText);
int decimalPlaces = Integer.valueOf(answerData.substring(answerData.indexOf(",")+1, answerData.length()));
return decimalPlaces;
}
/**
* CALCULATED_QUESTION
* This returns the "|2,2" (variance and decimal display) from the stored answer data.
* @param allAnswerText
* @return
*/
private String getAnswerData(String allAnswerText) {
String answerData = allAnswerText.substring(allAnswerText.indexOf("|"), allAnswerText.length());
return answerData;
}
/**
* CALCULATED_QUESTION
* This is just "(x+y)/z" or if values have been added to the expression it's the
* calculated value as stored in the answer data.
* @param allAnswerText
* @return
*/
private String getAnswerExpression(String allAnswerText) {
String answerExpression = allAnswerText.substring(0, allAnswerText.indexOf("|"));
return answerExpression;
}
/**
* CALCULATED_QUESTION
* Default acceptable variance and decimalPlaces. An answer is defined by an expression
* such as {x+y|1,2} if the variance and decimal places are left off. We have to default
* them to something.
*/
private String defaultVarianceAndDecimal(String allAnswerText) {
String defaultVariance = "0.001";
String defaultDecimal = "3";
if (!allAnswerText.contains("|")) {
if (!allAnswerText.contains(","))
allAnswerText = allAnswerText.concat("|"+defaultVariance+","+defaultDecimal);
else
allAnswerText = allAnswerText.replace(",","|"+defaultVariance+",");
}
if (!allAnswerText.contains(","))
allAnswerText = allAnswerText.concat(","+defaultDecimal);
return allAnswerText;
}
/**
* CALCULATED_QUESTION
* Takes an answer string and checks for the value returned
* is NaN or Infinity, indicating a Samigo formula parse error
* Returns false if divide by zero is detected.
*/
public boolean isAnswerValid(String answer) {
String INFINITY = "Infinity";
String NaN = "NaN";
if (answer.length() == 0) return false;
if (answer.equals(INFINITY)) return false;
return !answer.equals(NaN);
}
/**
* CALCULATED_QUESTION
* SAK-39922 - Support (or at least watch for support) for binary/unary calculated question (-1--1)
* SAK-40942 - Error in calculated questions: the decimal representation .n or n. (where n is a number) does not work
*/
private static String checkExpression(String expression) {
expression = CALCQ_ANSWER_AVOID_DOUBLE_MINUS.matcher(expression).replaceAll("+");
expression = CALCQ_ANSWER_AVOID_PLUS_MINUS.matcher(expression).replaceAll("-");
expression = CALCQ_FORMULA_ALLOW_POINT_NUMBER.matcher(expression).replaceAll("$10$2$3");
expression = CALCQ_FORMULA_ALLOW_NUMBER_POINT.matcher(expression).replaceAll("$1$3");
return expression;
}
/**
* CALCULATED_QUESTION
* replaceMappedVariablesWithNumbers() takes a string and substitutes any variable
* names found with the value of the variable. Variables look like {a}, the name of
* that variable is "a", and the value of that variable is in variablesWithValues
* Note - this function comprehends syntax like "5{x}". If "x" is 37, the
* result would be "5*37"
* @param expression - the string being substituted into
* @param variables - Map key is the variable name, value is what will be
* substituted into the expression.
* @return a string with values substituted. If answerExpression is null,
* returns a blank string (i.e ""). If variablesWithValues is null, returns
* the original answerExpression
*/
public String replaceMappedVariablesWithNumbers(String expression, Map variables) {
if (expression == null) {
expression = "";
}
if (variables == null) {
variables = new HashMap<>();
}
for (Map.Entry entry : variables.entrySet()) {
String name = "{" + entry.getKey() + "}";
String value = entry.getValue();
// not doing string replace or replaceAll because the value being
// substituted can change for each occurrence of the variable.
int index = expression.indexOf(name);
while (index > -1) {
String prefix = expression.substring(0, index);
String suffix = expression.substring(index + name.length());
String replacementValue = value;
// If replacement is a negative number, wrap in parentheses to avoid confused operations
// For example, 3 {y} with -10 should be 3*-10= -30 not 3 -10 = -7
if (value.charAt(0) == '-' && Character.isDigit(value.charAt(1))) {
replacementValue = ParserSymbol.LEFT_PARENTHESES_STR + value + ParserSymbol.RIGHT_PARENTHESES_STR;
}
// if last character of prefix is a number or the edge of parenthesis, multiply by the variable
// if x = 37, 5{x} -> 5*37
// if x = 37 (5+2){x} -> (5+2)*37 (prefix is (5+2)
if (prefix.length() > 0 && (Character.isDigit(prefix.charAt(prefix.length() - 1)) || prefix.charAt(prefix.length() - 1) == ')')) {
replacementValue = "*" + replacementValue;
}
// if first character of suffix is a number or the edge of parenthesis, multiply by the variable
// if x = 37, {x}5 -> 37*5
// if x = 37, {x}(5+2) -> 37*(5+2) (suffix is (5+2)
if (suffix.length() > 0 && (Character.isDigit(suffix.charAt(0)) || suffix.charAt(0) == '(')) {
replacementValue = replacementValue + "*";
}
// perform substitution, then look for the next instance of current variable
expression = prefix + replacementValue + suffix;
index = expression.indexOf(name);
}
}
return expression;
}
/**
* CALCULATED_QUESTION
* replaceMappedVariablesWithNumbers() takes a string and substitutes any variable
* names found with the value of the variable. Variables look like {a}, the name of
* that variable is "a", and the value of that variable is in variablesWithValues
* Note - this function comprehends syntax like "5{x}". If "x" is 37, the
* result would be "5*37"
* @param expression - the string which will be scanned for calculations
* @return the input string with calculations replaced with number values. If answerExpression is null,
* returns a blank string (i.e "") and if no calculations are found then original string is returned.
* @throws IllegalStateException if the formula value cannot be calculated
* @throws SamigoExpressionError if the formula cannot be parsed
*/
public String replaceCalculationsWithValues(String expression, int decimalPlaces) throws SamigoExpressionError {
if (StringUtils.isEmpty(expression)) {
expression = "";
} else {
String calculationAuxOpen = "" + CALCULATION_AUX_OPEN + CALCULATION_AUX_OPEN;
String calculationAuxClose = "" + CALCULATION_AUX_CLOSE + CALCULATION_AUX_CLOSE;
expression = expression.replaceAll("\\[\\[", calculationAuxOpen).replaceAll("\\]\\](?!\\])", calculationAuxClose);
Matcher keyMatcher = CALCQ_CALCULATION_PATTERN.matcher(expression);
List toReplace = new ArrayList<>();
while (keyMatcher.find()) {
String match = keyMatcher.group(1);
toReplace.add(match); // should be the formula
}
if (toReplace.size() > 0) {
for (String formula : toReplace) {
String replace = calculationAuxOpen + formula + calculationAuxClose;
formula = StringEscapeUtils.unescapeHtml4(formula);
String formulaValue = processFormulaIntoValue(formula, decimalPlaces);
expression = StringUtils.replace(expression, replace, formulaValue);
}
expression = expression.replaceAll(calculationAuxOpen, "\\[\\[").replaceAll(calculationAuxClose, "\\]\\]");
}
}
return expression;
}
/**
* CALCULATED_QUESTION
* Process a single formula into a final string representing the calculated value of the formula
*
* @param formula the formula to process (e.g. 1 * 2 + 3 - 4), All variable replacement must have already happened
* @param decimalPlaces number of decimals to include in the final output
* @return the value of the formula OR empty string if there is nothing to process
* @throws IllegalStateException if the formula value cannot be calculated (typically caused by 0 divisors and the like)
* @throws SamigoExpressionError if the formula cannot be parsed
*/
public String processFormulaIntoValue(String formula, int decimalPlaces) throws SamigoExpressionError {
String value = "";
if (StringUtils.isEmpty(formula)) {
value = "";
} else {
if (decimalPlaces < 0) {
decimalPlaces = 0;
}
formula = checkExpression(cleanFormula(formula));
SamigoExpressionParser parser = new SamigoExpressionParser(); // this will turn the expression into a number in string form
String numericString = parser.parse(formula, decimalPlaces+1);
if (this.isAnswerValid(numericString)) {
numericString = toScientificNotation(numericString, decimalPlaces);
value = numericString;
} else {
throw new IllegalStateException("Invalid calculation formula ("+formula+") result ("+numericString+"), result could not be calculated");
}
}
return value;
}
/**
* Cleans up formula text so that whitespaces are normalized or removed
* @param formula formula with variables or without
* @return the cleaned formula
*/
public static String cleanFormula(String formula) {
if (StringUtils.isEmpty(formula)) {
formula = "";
} else {
formula = StringUtils.trimToEmpty(formula).replaceAll("\\s+", " ");
}
return formula;
}
/**
* CALCULATED_QUESTION
* Takes a map of ranges and randomly chooses values for those ranges and stores them in a new map.
*/
public Map determineRandomValuesForRanges(Map variableRangeMap, long itemId, long gradingId, String agentId, int validAnswersAttemptCount) {
Map variableValueMap = new HashMap<>();
// seed random number generator
long seed = getCalcuatedQuestionSeed(itemId, gradingId, agentId, validAnswersAttemptCount);
Random generator = new Random(seed);
Iterator> i = variableRangeMap.entrySet().iterator();
while(i.hasNext())
{
Map.Entryentry = i.next();
String delimRange = entry.getValue(); // ie. "-100|100,2"
BigDecimal minVal = new BigDecimal(delimRange.substring(0, delimRange.indexOf('|')));
BigDecimal maxVal = new BigDecimal(delimRange.substring(delimRange.indexOf('|')+1, delimRange.indexOf(',')));
int decimalPlaces = Integer.valueOf(delimRange.substring(delimRange.indexOf(',')+1, delimRange.length()));
// This line does the magic of creating the random variable value within the range.
BigDecimal randomValue = maxVal.subtract(minVal).multiply(new BigDecimal(generator.nextDouble())).add(minVal);
// Trim off excess decimal points based on decimalPlaces value
/*BigDecimal bd = new BigDecimal(randomValue);
bd = bd.setScale(decimalPlaces,BigDecimal.ROUND_HALF_UP);
randomValue = bd.doubleValue();
String displayNumber = randomValue.toString();*/
String displayNumber = toScientificNotation(randomValue.toString(), decimalPlaces);
// Remove ".0" if decimalPlaces ==0
if (decimalPlaces == 0) {
displayNumber = displayNumber.replace(".0", "");
}
variableValueMap.put(entry.getKey(), displayNumber);
}
return variableValueMap;
}
/**
* CALCULATED_QUESTION
* Accepts an ItemDataIfc and returns a HashMap with the pairs of
* variable names and variable ranges.
*/
private Map buildVariableRangeMap(ItemDataIfc item) {
Map variableRangeMap = new HashMap<>();
String instructions = item.getInstruction();
List variables = this.extractVariables(instructions);
// Loop through each VarName
@SuppressWarnings("unchecked")
List itemTextList = item.getItemTextArraySorted();
for (ItemTextIfc varName : itemTextList) {
// only look at variables for substitution, ignore formulas
if (variables.contains(varName.getText())) {
@SuppressWarnings("unchecked")
List answerList = varName.getAnswerArray();
for (AnswerIfc range : answerList) {
if (!(range.getLabel() == null) ) { // answer records and variable records are in the same set
if (range.getSequence().equals(varName.getSequence()) && range.getText().contains("|")) {
variableRangeMap.put(varName.getText(), range.getText());
}
}
}
}
}
return variableRangeMap;
}
/**
* CALCULATED_QUESTION
* Make seed by combining user id, item (question) id, grading (submission) id, and attempt count (due to div by 0)
*/
private long getCalcuatedQuestionSeed(long itemId, long gradingId, String agentId, int validAnswersAttemptCount) {
long userSeed = (long) agentId.hashCode();
return userSeed * itemId * gradingId * validAnswersAttemptCount;
}
/**
* CALCULATED_QUESTION
* Simple to check to see if this is a calculated question. It's used in storeGrades() to see if the sort is necessary.
*/
private boolean isCalcQuestion(List tempItemGradinglist, Map publishedItemHash) {
if (tempItemGradinglist == null) return false;
if (tempItemGradinglist.isEmpty()) return false;
Iterator iter = tempItemGradinglist.iterator();
while(iter.hasNext()){
ItemGradingData itemCheck = (ItemGradingData) iter.next();
Long itemId = itemCheck.getPublishedItemId();
ItemDataIfc item = (ItemDataIfc) publishedItemHash.get(itemId);
if (item != null && (TypeIfc.CALCULATED_QUESTION).equals(item.getTypeId())) {
return true;
}
}
return false;
}
/**
* IMAGE_MAP QUESTION
* Simple to check to see if this is a calculated question. It's used in storeGrades() to see if the sort is necessary.
*/
private boolean isImageMapQuestion(List tempItemGradinglist, Map publishedItemHash) {
if (tempItemGradinglist == null) return false;
if (tempItemGradinglist.isEmpty()) return false;
for(ItemGradingData itemCheck: tempItemGradinglist){
Long itemId = itemCheck.getPublishedItemId();
ItemDataIfc item = (ItemDataIfc) publishedItemHash.get(itemId);
if (item != null && (TypeIfc.IMAGEMAP_QUESTION).equals(item.getTypeId())) {
return true;
}
}
return false;
}
/**
* FIB QUESTIONS
* we ensure that answer text between brackets is always nonempty, also that
* starting text is nonempty, we use a non-breaking space for this purpose
* @param fib
* @return
*/
private static String padFibWithNonbreakSpacesText(String fib, String marker_left, String marker_right) {
String fibTmp=fib;
if (fibTmp.startsWith(marker_left)){
fibTmp = NBSP + fibTmp;
}
String regExp = Pattern.quote("" + marker_right) + Pattern.quote("" + marker_left);
return fibTmp.replaceAll(regExp, marker_right + NBSP + marker_left);
}
/**
* FIB QUESTIONS
* Get list of form:
* {ans=red, text=Roses are},
* {ans=blue, text=and violets are},
* {ans=null, text=.}
* From String of form "Roses are {red} and violets are {blue}."
*
* @param input
* @return list of Maps
*/
public List