/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) frentix GmbH<br>
* http://www.frentix.com<br>
* <p>
*/
package org.olat.ims.qti21.manager;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.DoubleAdder;
import javax.persistence.TypedQuery;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.persistence.PersistenceHelper;
import org.olat.ims.qti.statistics.manager.Statistics;
import org.olat.ims.qti.statistics.model.StatisticAssessment;
import org.olat.ims.qti.statistics.model.StatisticsItem;
import org.olat.ims.qti21.QTI21StatisticsManager;
import org.olat.ims.qti21.model.QTI21StatisticSearchParams;
import org.olat.ims.qti21.model.statistics.AbstractTextEntryInteractionStatistics;
import org.olat.ims.qti21.model.statistics.AssessmentItemStatistic;
import org.olat.ims.qti21.model.statistics.ChoiceStatistics;
import org.olat.ims.qti21.model.statistics.HotspotChoiceStatistics;
import org.olat.ims.qti21.model.statistics.KPrimStatistics;
import org.olat.ims.qti21.model.statistics.MatchStatistics;
import org.olat.ims.qti21.model.statistics.NumericalInputInteractionStatistics;
import org.olat.ims.qti21.model.statistics.TextEntryInteractionStatistics;
import org.olat.ims.qti21.model.xml.QtiNodesExtractor;
import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.NumericalEntry;
import org.olat.modules.vitero.model.GroupRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.ChoiceInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.HotspotInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.HottextInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.MatchInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.TextEntryInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleAssociableChoice;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleChoice;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleMatchSet;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.content.Hottext;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.graphic.HotspotChoice;
import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.MapEntry;
import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration;
import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef;
import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem;
import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest;
import uk.ac.ed.ph.jqtiplus.types.Identifier;
import uk.ac.ed.ph.jqtiplus.utils.QueryUtils;
import uk.ac.ed.ph.jqtiplus.value.BaseType;
import uk.ac.ed.ph.jqtiplus.value.DirectedPairValue;
import uk.ac.ed.ph.jqtiplus.value.SingleValue;
import uk.ac.ed.ph.jqtiplus.value.StringValue;
/**
*
* Initial date: 24.07.2015<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
@Service
public class QTI21StatisticsManagerImpl implements QTI21StatisticsManager {
@Autowired
private DB dbInstance;
private StringBuilder decorateRSet(StringBuilder sb, QTI21StatisticSearchParams searchParams, boolean finished) {
sb.append(" where asession.testEntry.key=:testEntryKey and asession.repositoryEntry.key=:repositoryEntryKey");
if(searchParams.getNodeIdent() != null ) {
sb.append(" and asession.subIdent=:subIdent");
} else {
sb.append(" and asession.subIdent is null");
}
sb.append(" and asession.authorMode=false");
if(finished) {
sb.append(" and asession.finishTime is not null");
}
sb.append(" and asession.lastModified = (select max(a2session.lastModified) from qtiassessmenttestsession a2session")
.append(" where asession.testEntry.key=a2session.testEntry.key and a2session.repositoryEntry.key=asession.repositoryEntry.key");
if(searchParams.getNodeIdent() != null ) {
sb.append(" and a2session.subIdent=asession.subIdent");
} else {
sb.append(" and asession.subIdent is null and a2session.subIdent is null");
}
sb.append(" and (a2session.identity.key=asession.identity.key or a2session.anonymousIdentifier=asession.anonymousIdentifier)")
.append(" )");
if(searchParams.isViewAllUsers() && searchParams.isViewAnonymUsers()) {
//no restrictions
} else if(searchParams.isViewAnonymUsers()) {
sb.append(" and asession.anonymousIdentifier is not null");
} else if(searchParams.isViewAllUsers()) {
sb.append(" and asession.identity.key in (select data.identity.key from assessmententry data")
.append(" where data.repositoryEntry.key=asession.repositoryEntry.key")
.append(" )");
} else if(searchParams.getLimitToGroups() != null && searchParams.getLimitToGroups().size() > 0) {
sb.append(" and asession.identity.key in ( select membership.identity.key from bgroupmember membership")
.append(" where membership.group in (:baseGroups) and membership.role='").append(GroupRole.participant).append("'")
.append(" )");
} else {
//limit to participants
sb.append(" and (asession.identity.key in ( select membership.identity.key from repoentrytogroup as rel, bgroupmember membership ")
.append(" where rel.entry.key=:repositoryEntryKey and rel.group.key=membership.group.key and membership.role='").append(GroupRole.participant).append("'")
//.append(" where rel.entry.key=:repositoryEntryKey and rel.group.key=reBaseGroup.key and membership.group.key=reBaseGroup.key and membership.role='").append(GroupRole.participant).append("'")
.append(" )");
// add non members
if(searchParams.isViewNonMembers()) {
sb.append(" or asession.identity.key not in (select membership.identity.key from repoentrytogroup as rel, bgroupmember as membership")
.append(" where rel.entry.key=:repositoryEntryKey and rel.group.key=membership.group.key")
.append(" )");
}
sb.append(")");
}
return sb;
}
private void decorateRSetQuery(TypedQuery<?> query, QTI21StatisticSearchParams searchParams) {
query.setParameter("testEntryKey", searchParams.getTestEntry().getKey());
if(searchParams.getCourseEntry() == null) {
query.setParameter("repositoryEntryKey", searchParams.getTestEntry().getKey());
} else {
query.setParameter("repositoryEntryKey", searchParams.getCourseEntry().getKey());
}
if(searchParams.getNodeIdent() != null ) {
query.setParameter("subIdent", searchParams.getNodeIdent());
}
if(searchParams.isViewAllUsers() && searchParams.isViewAnonymUsers()) {
//no restrictions
} else if(searchParams.isViewAnonymUsers()) {
//
} else if(searchParams.isViewAllUsers()) {
//
} else if(searchParams.getLimitToGroups() != null && searchParams.getLimitToGroups().size() > 0) {
query.setParameter("baseGroups", searchParams.getLimitToGroups());
}
}
@Override
public StatisticAssessment getAssessmentStatistics(QTI21StatisticSearchParams searchParams) {
StringBuilder sb = new StringBuilder();
sb.append("select asession.score, asession.manualScore, asession.passed, asession.duration from qtiassessmenttestsession asession ");
decorateRSet(sb, searchParams, true);
sb.append(" order by asession.key asc");
TypedQuery<Object[]> rawDataQuery = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Object[].class);
decorateRSetQuery(rawDataQuery, searchParams);
List<Object[]> rawDatas = rawDataQuery.getResultList();
int numOfPassed = 0;
int numOfFailed = 0;
double totalDuration = 0.0;
double maxScore = 0.0;
double minScore = Double.MAX_VALUE;
double[] scores = new double[rawDatas.size()];
double[] durationSeconds = new double[rawDatas.size()];
double minDuration = Double.MAX_VALUE;
double maxDuration = 0d;
int dataPos = 0;
boolean hasScore = false;
for(Object[] rawData:rawDatas) {
int pos = 0;
BigDecimal score = (BigDecimal)rawData[pos++];
BigDecimal manualScore = (BigDecimal)rawData[pos++];
if(score == null) {
score = manualScore;
} else if(manualScore != null) {
score = score.add(manualScore);
}
if(score != null) {
double scored = score.doubleValue();
scores[dataPos] = scored;
maxScore = Math.max(maxScore, scored);
minScore = Math.min(minScore, scored);
hasScore = true;
}
Boolean passed = (Boolean)rawData[pos++];
if(passed != null) {
if(passed.booleanValue()) {
numOfPassed++;
} else {
numOfFailed++;
}
}
Long duration = (Long)rawData[pos++];
if(duration != null) {
double durationd = duration.doubleValue();
double durationSecond = Math.round(durationd / 1000d);
durationSeconds[dataPos] = durationSecond;
totalDuration += durationd;
minDuration = Math.min(minDuration, durationSecond);
maxDuration = Math.max(maxDuration, durationSecond);
}
dataPos++;
}
if (rawDatas.size() == 0) {
minScore = 0;
}
Statistics statisticsHelper = new Statistics(scores);
int numOfParticipants = rawDatas.size();
StatisticAssessment stats = new StatisticAssessment();
stats.setNumOfParticipants(numOfParticipants);
stats.setNumOfPassed(numOfPassed);
stats.setNumOfFailed(numOfFailed);
long averageDuration = Math.round(totalDuration / numOfParticipants);
stats.setAverageDuration(averageDuration);
stats.setAverage(statisticsHelper.getMean());
if(hasScore) {
double range = maxScore - minScore;
stats.setRange(range);
stats.setMaxScore(maxScore);
stats.setMinScore(minScore);
}
stats.setStandardDeviation(statisticsHelper.getStdDev());
stats.setMedian(statisticsHelper.median());
stats.setMode(statisticsHelper.mode());
stats.setScores(scores);
stats.setDurations(durationSeconds);
return stats;
}
@Override
public StatisticsItem getAssessmentItemStatistics(String itemIdent, double maxScore,
QTI21StatisticSearchParams searchParams) {
StringBuilder sb = new StringBuilder();
sb.append("select isession.score, isession.manualScore, count(isession.key), avg(isession.duration) from qtiassessmentitemsession isession ")
.append(" inner join isession.assessmentTestSession asession");
decorateRSet(sb, searchParams, true);
sb.append(" and isession.assessmentItemIdentifier=:itemIdent and isession.duration > 0")
.append(" group by isession.score, isession.manualScore");
TypedQuery<Object[]> query = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Object[].class)
.setParameter("itemIdent", itemIdent);
decorateRSetQuery(query, searchParams);
List<Object[]> results = query.getResultList();
if(results.isEmpty()) {
return new StatisticsItem();
}
int totalResults = 0;
double totalScore = 0.0;
double totalDuration = 0.0;
long numOfCorrectAnswers = 0;
long numOfIncorrectAnswers = 0;
for(Object[] result:results) {
BigDecimal score = (BigDecimal)result[0];
BigDecimal manualScore = (BigDecimal)result[1];
if(score == null) {
score = manualScore;
} else if(manualScore != null) {
score = score.add(manualScore);
}
long numOfResults = ((Number)result[2]).longValue();
double averageDuration = ((Number)result[3]).doubleValue();
//average
double dScore = score == null ? 0.0d : score.doubleValue();
totalScore += (dScore * numOfResults);
totalResults += numOfResults;
if((maxScore - dScore) < 0.0001) {
numOfCorrectAnswers += numOfResults;
} else {
numOfIncorrectAnswers += numOfResults;
}
totalDuration += (averageDuration * numOfResults);
}
double averageScore = totalScore / totalResults;
//difficulty (p-value)
double difficulty = numOfCorrectAnswers / (double)totalResults;
double averageDuration = totalDuration / totalResults;
StatisticsItem stats = new StatisticsItem();
stats.setAverageDuration(Math.round(averageDuration));
stats.setAverageScore(averageScore);
stats.setNumOfResults(totalResults);
stats.setDifficulty(difficulty);
stats.setNumOfCorrectAnswers(numOfCorrectAnswers);
stats.setNumOfIncorrectAnswers(numOfIncorrectAnswers);
return stats;
}
@Override
public List<ChoiceStatistics> getChoiceInteractionStatistics(String itemRefIdent,
AssessmentItem assessmentItem, ChoiceInteraction choiceInteraction, QTI21StatisticSearchParams searchParams) {
List<RawData> results = getRawDatas(itemRefIdent, choiceInteraction.getResponseIdentifier().toString(), searchParams);
List<SimpleChoice> simpleChoices = choiceInteraction.getSimpleChoices();
long[] counts = new long[simpleChoices.size()];
for(int i=counts.length; i-->0; ) {
counts[i] = 0l;
}
for(RawData result:results) {
Long numOfAnswers = result.getCount();;
if(numOfAnswers != null && numOfAnswers.longValue() > 0) {
String stringuifiedResponse = result.getStringuifiedResponse();
for(int i=simpleChoices.size(); i-->0; ) {
String identifier = simpleChoices.get(i).getIdentifier().toString();
if(stringuifiedResponse.contains(identifier)) {
counts[i] += numOfAnswers.longValue();
}
}
}
}
List<ChoiceStatistics> choicesStatistics = new ArrayList<>();
for(int i=0; i<simpleChoices.size(); i++) {
choicesStatistics.add(new ChoiceStatistics(simpleChoices.get(i), counts[i]));
}
return choicesStatistics;
}
@Override
public List<ChoiceStatistics> getHottextInteractionStatistics(String itemRefIdent,
AssessmentItem assessmentItem, HottextInteraction hottextInteraction,
QTI21StatisticSearchParams searchParams) {
List<RawData> results = getRawDatas(itemRefIdent, hottextInteraction.getResponseIdentifier().toString(), searchParams);
List<Hottext> hottexts = QueryUtils.search(Hottext.class, hottextInteraction);
long[] counts = new long[hottexts.size()];
for(int i=counts.length; i-->0; ) {
counts[i] = 0l;
}
for(RawData result:results) {
Long numOfAnswers = result.getCount();;
if(numOfAnswers != null && numOfAnswers.longValue() > 0) {
String stringuifiedResponse = result.getStringuifiedResponse();
for(int i=hottexts.size(); i-->0; ) {
String identifier = hottexts.get(i).getIdentifier().toString();
if(stringuifiedResponse.contains(identifier)) {
counts[i] += numOfAnswers.longValue();
}
}
}
}
List<ChoiceStatistics> choicesStatistics = new ArrayList<>();
for(int i=0; i<hottexts.size(); i++) {
choicesStatistics.add(new ChoiceStatistics(hottexts.get(i), counts[i]));
}
return choicesStatistics;
}
@Override
public List<HotspotChoiceStatistics> getHotspotInteractionStatistics(String itemRefIdent,
AssessmentItem assessmentItem, HotspotInteraction hotspotInteraction,
QTI21StatisticSearchParams searchParams) {
List<RawData> results = getRawDatas(itemRefIdent, hotspotInteraction.getResponseIdentifier().toString(), searchParams);
List<HotspotChoice> hotspotChoices = hotspotInteraction.getHotspotChoices();
long[] counts = new long[hotspotChoices.size()];
for(int i=counts.length; i-->0; ) {
counts[i] = 0l;
}
for(RawData result:results) {
Long numOfAnswers = result.getCount();;
if(numOfAnswers != null && numOfAnswers.longValue() > 0) {
String stringuifiedResponse = result.getStringuifiedResponse();
for(int i=hotspotChoices.size(); i-->0; ) {
String identifier = hotspotChoices.get(i).getIdentifier().toString();
if(stringuifiedResponse.contains(identifier)) {
counts[i] += numOfAnswers.longValue();
}
}
}
}
List<HotspotChoiceStatistics> choicesStatistics = new ArrayList<>();
for(int i=0; i<hotspotChoices.size(); i++) {
choicesStatistics.add(new HotspotChoiceStatistics(hotspotChoices.get(i), counts[i]));
}
return choicesStatistics;
}
//stringuifiedResponse: [a93247453265982 correct][b93247453265983 correct][c93247453265984 correct][d93247453265985 correct]
@Override
public List<KPrimStatistics> getKPrimStatistics(String itemRefIdent, AssessmentItem item, MatchInteraction interaction,
QTI21StatisticSearchParams searchParams) {
List<RawData> rawDatas = getRawDatas(itemRefIdent, interaction.getResponseIdentifier().toString(), searchParams);
List<SimpleMatchSet> matchSets = interaction.getSimpleMatchSets();
List<KPrimStatistics> kprimPoints = new ArrayList<>();
SimpleMatchSet fourMatchSet = matchSets.get(0);
ResponseDeclaration responseDeclaration = item.getResponseDeclaration(interaction.getResponseIdentifier());
//readable responses
Set<String> rightResponses = new HashSet<>();
List<MapEntry> mapEntries = responseDeclaration.getMapping().getMapEntries();
for(MapEntry mapEntry:mapEntries) {
SingleValue mapKey = mapEntry.getMapKey();
if(mapKey instanceof DirectedPairValue) {
DirectedPairValue pairValue = (DirectedPairValue)mapKey;
String source = pairValue.sourceValue().toString();
String destination = pairValue.destValue().toString();
rightResponses.add("[" + source + " " + destination + "]");
}
}
for(SimpleAssociableChoice choice:fourMatchSet.getSimpleAssociableChoices()) {
String choiceIdentifier = choice.getIdentifier().toString();
String markerCorrect = "[" + choiceIdentifier + " correct]";
String markerWrong = "[" + choiceIdentifier + " wrong]";
boolean isCorrectRight = rightResponses.contains(markerCorrect);
String rightFlag = isCorrectRight ? markerCorrect : markerWrong;
String wrongFlag = isCorrectRight ? markerWrong : markerCorrect;
long numCorrect = 0;
long numIncorrect = 0;
long numUnanswered = 0;
for(RawData rawData:rawDatas) {
String response = rawData.getStringuifiedResponse();
if(response.indexOf(rightFlag) >= 0) {
numCorrect += rawData.getCount();
} else if(response.indexOf(wrongFlag) >= 0) {
numIncorrect += rawData.getCount();
} else {
numUnanswered += rawData.getCount();
}
}
kprimPoints.add(new KPrimStatistics(choice.getIdentifier(), isCorrectRight, numCorrect, numIncorrect, numUnanswered));
}
return kprimPoints;
}
@Override
public List<MatchStatistics> getMatchStatistics(String itemRefIdent, AssessmentItem item, MatchInteraction interaction,
QTI21StatisticSearchParams searchParams) {
List<RawData> rawDatas = getRawDatas(itemRefIdent, interaction.getResponseIdentifier().toString(), searchParams);
SimpleMatchSet sourceMatchSets = interaction.getSimpleMatchSets().get(0);
SimpleMatchSet targetMatchSets = interaction.getSimpleMatchSets().get(1);
List<MatchStatistics> matchPoints = new ArrayList<>();
ResponseDeclaration responseDeclaration = item.getResponseDeclaration(interaction.getResponseIdentifier());
//readable responses
Map<Identifier,List<Identifier>> associations = new HashMap<>();
CorrectResponse correctResponse = responseDeclaration.getCorrectResponse();
QtiNodesExtractor.extractIdentifiersFromCorrectResponse(correctResponse, associations);
for(SimpleAssociableChoice sourceChoice:sourceMatchSets.getSimpleAssociableChoices()) {
for(SimpleAssociableChoice targetChoice:targetMatchSets.getSimpleAssociableChoices()) {
DirectedPairValue dKey = new DirectedPairValue(sourceChoice.getIdentifier(), targetChoice.getIdentifier());
String choiceIdentifier = dKey.toQtiString();
String marker = "[" + choiceIdentifier + "]";
boolean correct = associations.containsKey(sourceChoice.getIdentifier())
&& associations.get(sourceChoice.getIdentifier()).contains(targetChoice.getIdentifier());
long numCorrect = 0;
long numIncorrect = 0;
for(RawData rawData:rawDatas) {
String response = rawData.getStringuifiedResponse();
if(response.indexOf(marker) >= 0) {
if(correct) {
numCorrect += rawData.getCount();
} else {
numIncorrect += rawData.getCount();
}
}
}
matchPoints.add(new MatchStatistics(sourceChoice.getIdentifier(), targetChoice.getIdentifier(), numCorrect, numIncorrect));
}
}
return matchPoints;
}
@Override
public List<AbstractTextEntryInteractionStatistics> getTextEntryInteractionsStatistic(String itemRefIdent, AssessmentItem item, List<TextEntryInteraction> interactions,
QTI21StatisticSearchParams searchParams) {
List<AbstractTextEntryInteractionStatistics> options = new ArrayList<>();
Map<String, AbstractTextEntryInteractionStatistics> optionMap = new HashMap<>();
for(TextEntryInteraction interaction:interactions) {
Identifier responseIdentifier = interaction.getResponseIdentifier();
ResponseDeclaration responseDeclaration = item.getResponseDeclaration(responseIdentifier);
if(responseDeclaration.hasBaseType(BaseType.STRING)) {
TextEntryInteractionStatistics stats = getTextEntryInteractionSettings(responseIdentifier, responseDeclaration);
optionMap.put(responseIdentifier.toString(), stats);
options.add(stats);
} else if(responseDeclaration.hasBaseType(BaseType.FLOAT)) {
NumericalInputInteractionStatistics stats = getNumericalInputInteractionSettings(responseIdentifier, responseDeclaration, item);
optionMap.put(responseIdentifier.toString(), stats);
options.add(stats);
}
}
for(TextEntryInteraction interaction:interactions) {
String responseIdentifier = interaction.getResponseIdentifier().toString();
List<RawData> datas = getRawDatas(itemRefIdent, responseIdentifier, searchParams);
for(RawData data:datas) {
Long count = data.getCount();
if(count != null && count.longValue() > 0) {
AbstractTextEntryInteractionStatistics stats = optionMap.get(responseIdentifier);
String response = data.getStringuifiedResponse();
if(response != null && response.length() >= 2 && response.startsWith("[") && response.endsWith("]")) {
response = response.substring(1, response.length() - 1);
}
if(stats.matchResponse(response)) {
stats.addCorrect(count.longValue());
} else {
stats.addIncorrect(count.longValue());
stats.addWrongResponses(response);
}
}
}
}
return options;
}
private NumericalInputInteractionStatistics getNumericalInputInteractionSettings(Identifier responseIdentifier, ResponseDeclaration responseDeclaration, AssessmentItem item) {
NumericalEntry numericalEntry = new NumericalEntry(responseIdentifier);
FIBAssessmentItemBuilder.extractNumericalEntrySettings(item, numericalEntry, responseDeclaration, new AtomicInteger(), new DoubleAdder());
String correctResponse = "";
Double solution = numericalEntry.getSolution();
if(numericalEntry.getSolution() != null) {
correctResponse = solution.toString();
}
double points = Double.NaN;
if(numericalEntry.getScore() == null) {
points = 0.0d;//all score
} else {
points = numericalEntry.getScore().doubleValue();
}
return new NumericalInputInteractionStatistics(responseIdentifier, correctResponse, solution,
numericalEntry.getToleranceMode(), numericalEntry.getLowerTolerance(), numericalEntry.getUpperTolerance(),
points);
}
private TextEntryInteractionStatistics getTextEntryInteractionSettings(Identifier responseIdentifier, ResponseDeclaration responseDeclaration) {
String correctResponse = null;
boolean caseSensitive = true;
double points = Double.NaN;
List<String> alternatives = new ArrayList<>();
List<MapEntry> mapEntries = responseDeclaration.getMapping().getMapEntries();
for(MapEntry mapEntry:mapEntries) {
SingleValue mapKey = mapEntry.getMapKey();
if(mapKey instanceof StringValue) {
String value = ((StringValue)mapKey).stringValue();
if(correctResponse == null) {
correctResponse = value;
points = mapEntry.getMappedValue();
} else {
alternatives.add(value);
}
}
caseSensitive &= mapEntry.getCaseSensitive();
}
if(points == -1.0d) {
points = 0.0d;//all score
}
return new TextEntryInteractionStatistics(responseIdentifier, caseSensitive, correctResponse, alternatives, points);
}
private List<RawData> getRawDatas(String itemRefIdent, String responseIdentifier, QTI21StatisticSearchParams searchParams) {
StringBuilder sb = new StringBuilder();
sb.append("select isession.key, aresponse.responseIdentifier, aresponse.stringuifiedResponse, count(aresponse.key) from qtiassessmentresponse aresponse ")
.append(" inner join aresponse.assessmentItemSession isession")
.append(" inner join isession.assessmentTestSession asession");
decorateRSet(sb, searchParams, true);
sb.append(" and isession.assessmentItemIdentifier=:itemIdent and aresponse.responseIdentifier=:responseIdentifier and isession.duration > 0")
.append(" group by isession.key, aresponse.responseIdentifier, aresponse.stringuifiedResponse");
TypedQuery<Object[]> query = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Object[].class)
.setParameter("itemIdent", itemRefIdent)
.setParameter("responseIdentifier", responseIdentifier);
decorateRSetQuery(query, searchParams);
List<Object[]> results = query.getResultList();
if(results.isEmpty()) {
return new ArrayList<>();
}
List<RawData> datas = new ArrayList<>(results.size());
for(Object[] result:results) {
Long itemSessionKey = PersistenceHelper.extractLong(result, 0);
String stringuifiedResponse = PersistenceHelper.extractString(result, 2);
Long count = PersistenceHelper.extractLong(result, 3);
datas.add(new RawData(itemSessionKey, responseIdentifier, stringuifiedResponse, count));
}
return datas;
}
@Override
public List<AssessmentItemStatistic> getStatisticPerItem(ResolvedAssessmentTest resolvedAssessmentTest, QTI21StatisticSearchParams searchParams, double numOfParticipants) {
StringBuilder sb = new StringBuilder();
sb.append("select isession.assessmentItemIdentifier, isession.score, isession.manualScore, count(*) from qtiassessmentitemsession isession")
.append(" inner join isession.assessmentTestSession asession");
decorateRSet(sb, searchParams, true);
sb.append(" and isession.duration > 0")
.append(" group by isession.assessmentItemIdentifier, isession.score, isession.manualScore");
TypedQuery<Object[]> query = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Object[].class);
decorateRSetQuery(query, searchParams);
List<Object[]> results = query.getResultList();
if(results.isEmpty()) {
return new ArrayList<>();
}
Map<String,AssessmentItemRef> itemMap = new HashMap<>();
for(AssessmentItemRef itemRef:resolvedAssessmentTest.getAssessmentItemRefs()) {
itemMap.put(itemRef.getIdentifier().toString(), itemRef);
}
Map<String, AssessmentItemHelper> identifierToHelpers = new HashMap<>();
for(Object[] result:results) {
int pos = 0;
String identifier = PersistenceHelper.extractString(result, pos++);
BigDecimal score = (BigDecimal)result[pos++];
BigDecimal manualScore = (BigDecimal)result[pos++];
Long count = PersistenceHelper.extractLong(result, pos++);
if(score == null || identifier == null || count == null) {
continue;
}
AssessmentItemHelper helper = identifierToHelpers.get(identifier);
if(helper == null) {
AssessmentItemRef itemRef = itemMap.get(identifier);
if(itemRef == null) {
continue;
}
ResolvedAssessmentItem item = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
if(item == null) {
continue;
}
helper = new AssessmentItemHelper(item.getRootNodeLookup().extractIfSuccessful());
identifierToHelpers.put(identifier, helper);
}
helper.addCount(count);
if(manualScore != null) {
helper.addTotalScore(count, manualScore);
} else {
helper.addTotalScore(count, score);
}
if(helper.getMaxScore() != null) {
double maxValue = helper.getMaxScore().doubleValue();
if(Math.abs(score.doubleValue() - maxValue) < 0.0001) {
helper.addCorrectAnswers(count);
}
}
}
List<AssessmentItemStatistic> statistics = new ArrayList<>(identifierToHelpers.size());
for(AssessmentItemHelper helper:identifierToHelpers.values()) {
long numOfAnswersItem = helper.count;
long numOfCorrectAnswers = helper.countCorrectAnswers;
double average = (helper.totalScore / helper.count);
double averageParticipants = (helper.totalScore / numOfParticipants);
statistics.add(new AssessmentItemStatistic(helper.getAssessmentItem(), average, averageParticipants, numOfAnswersItem, numOfCorrectAnswers));
}
return statistics;
}
public static class AssessmentItemHelper {
private long count = 0l;
private double totalScore = 0.0d;
private Double maxScore;
private long countCorrectAnswers = 0;
private final AssessmentItem assessmentItem;
public AssessmentItemHelper(AssessmentItem assessmentItem) {
this.assessmentItem = assessmentItem;
if(assessmentItem != null) {
maxScore = QtiNodesExtractor.extractMaxScore(assessmentItem);
}
}
public AssessmentItem getAssessmentItem() {
return assessmentItem;
}
public Double getMaxScore() {
return maxScore;
}
public void addTotalScore(Long numOfAnswers, BigDecimal score) {
if(numOfAnswers != null && score != null) {
totalScore += (numOfAnswers.doubleValue() * score.doubleValue());
}
}
public void addCount(Long toAdd) {
if(toAdd != null) {
count += toAdd.longValue();
}
}
public void addCorrectAnswers(Long toAdd) {
if(toAdd != null) {
countCorrectAnswers += toAdd.longValue();
}
}
}
public static class RawData {
private final Long itemSessionKey;
private final String responseIdentifier;
private final String stringuifiedResponse;
private final Long count;
public RawData(Long itemSessionKey, String responseIdentifier, String stringuifiedResponse, Long count) {
this.itemSessionKey = itemSessionKey;
this.responseIdentifier = responseIdentifier;
this.stringuifiedResponse = stringuifiedResponse;
this.count = count;
}
public Long getItemSessionKey() {
return itemSessionKey;
}
public String getResponseIdentifier() {
return responseIdentifier;
}
public String getStringuifiedResponse() {
return stringuifiedResponse;
}
public Long getCount() {
return count;
}
}
}