/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
*/
package org.olat.modules.iq;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.dom4j.Document;
import org.olat.basesecurity.BaseSecurityManager;
import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
import org.olat.core.commons.persistence.DB;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.panel.Panel;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.generic.layout.GenericMainController;
import org.olat.core.gui.control.generic.layout.MainLayoutController;
import org.olat.core.gui.control.generic.messages.MessageUIFactory;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.OlatResourceableType;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.util.Util;
import org.olat.core.util.controller.OLATResourceableListeningWrapperController;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.LockResult;
import org.olat.core.util.vfs.LocalFolderImpl;
import org.olat.core.util.vfs.VFSConstants;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSManager;
import org.olat.core.util.vfs.VFSStatus;
import org.olat.course.nodes.iq.IQEditController;
import org.olat.ims.qti.QTIResult;
import org.olat.ims.qti.QTIResultSet;
import org.olat.ims.qti.container.AssessmentContext;
import org.olat.ims.qti.container.HttpItemInput;
import org.olat.ims.qti.container.ItemContext;
import org.olat.ims.qti.container.ItemInput;
import org.olat.ims.qti.container.ItemsInput;
import org.olat.ims.qti.container.SectionContext;
import org.olat.ims.qti.navigator.NavigatorDelegate;
import org.olat.ims.qti.process.AssessmentInstance;
import org.olat.ims.qti.process.FilePersister;
import org.olat.ims.qti.process.Resolver;
import org.olat.ims.qti.render.LocalizedXSLTransformer;
import org.olat.ims.qti.render.ResultsBuilder;
import org.olat.modules.ModuleConfiguration;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.RepositoryManager;
import org.olat.user.UserDataDeletable;
import org.olat.user.UserManager;
import org.olat.util.logging.activity.LoggingResourceable;
/**
* Initial Date: Mar 4, 2004
* @author Mike Stock
*/
public class IQManager implements UserDataDeletable {
private OLog log = Tracing.createLoggerFor(IQManager.class);
private DB dbInstance;
private UserManager userManager;
public void setDbInstance(DB dbInstance) {
this.dbInstance = dbInstance;
}
/**
* [user by Spring]
* @param userManager
*/
public void setUserManager(UserManager userManager) {
this.userManager = userManager;
}
/**
* IMS QTI Display Controller from within course -> moduleConfiguration
*
* concurrent access check needed -> Editor may save (commit changes) while displaying reads old/new data mix (files and xml structure)
*
*/
public Controller createIQDisplayController(ModuleConfiguration moduleConfiguration, IQSecurityCallback secCallback, UserRequest ureq,
WindowControl wControl, long courseResId, String courseNodeIdent, NavigatorDelegate delegate) {
//two cases:
// -- VERY RARE CASE -- 1) qti is open in an editor session right now on the screen (or session on the way to timeout)
// -- 99% of cases -- 2) qti is ready to be run as test/survey
String repositorySoftkey = (String) moduleConfiguration.get(IQEditController.CONFIG_KEY_REPOSITORY_SOFTKEY);
RepositoryEntry re = RepositoryManager.getInstance().lookupRepositoryEntryBySoftkey(repositorySoftkey, false);
if(re == null) {
log.error("The test repository entry with this soft key could not be found: " + repositorySoftkey);
Translator translator = Util.createPackageTranslator(this.getClass(), ureq.getLocale());
String title = translator.translate("error.test.deleted.title");
String msg = translator.translate("error.test.deleted.msg");
return MessageUIFactory.createInfoMessage(ureq, wControl, title, msg);
} else if (CoordinatorManager.getInstance().getCoordinator().getLocker().isLocked(re.getOlatResource(), null)){
Translator translator = Util.createPackageTranslator(this.getClass(), ureq.getLocale());
//so this resource is locked, let's find out who locked it
LockResult lockResult = CoordinatorManager.getInstance().getCoordinator().getLocker().acquireLock(re.getOlatResource(), ureq.getIdentity(), null);
String fullName = userManager.getUserDisplayName(lockResult.getOwner());
return MessageUIFactory.createInfoMessage(ureq, wControl, translator.translate("status.currently.locked.title"),
translator.translate("status.currently.locked", new String[] { fullName }));
}else{
ThreadLocalUserActivityLogger.addLoggingResourceInfo(LoggingResourceable.wrap(re, OlatResourceableType.iq));
return new IQDisplayController(moduleConfiguration, secCallback, ureq, wControl, courseResId, courseNodeIdent, delegate);
}
}
/**
* IMS QTI Display Controller used by QTI Editor for preview.
*
* no concurrency protection needed here -> it is Editor <-> Preview of edited file
*
* @param resolver
* @param type
* @param secCallback
* @param ureq
* @param wControl
*/
public IQDisplayController createIQDisplayController(Resolver resolver, String type, IQSecurityCallback secCallback,
UserRequest ureq, WindowControl wControl) {
return new IQDisplayController(resolver, type, secCallback, ureq, wControl);
}
/**
* IMS QTI Display Controller used for IMS course node run view, or for the direct launching from learning resources.
*
* concurrent access check needed -> Editor may save (commit changes) while displaying reads old/new data mix (files and xml structure)
*
*
* @param res
* @param resolver
* @param type
* @param secCallback
* @param ureq
* @param wControl
* @return
*/
public MainLayoutController createIQDisplayController(OLATResourceable res, Resolver resolver, String type, IQSecurityCallback secCallback,
UserRequest ureq, WindowControl wControl) {
ThreadLocalUserActivityLogger.addLoggingResourceInfo(LoggingResourceable.wrap(res, OlatResourceableType.iq));
//two cases:
// -- VERY RARE CASE -- 1) qti is open in an editor session right now on the screen (or session on the way to timeout)
// -- 99% of cases -- 2) qti is ready to be run as test/survey
if (CoordinatorManager.getInstance().getCoordinator().getLocker().isLocked(res, null)){
LockResult lockEntry = CoordinatorManager.getInstance().getCoordinator().getLocker().aquirePersistentLock(res, ureq.getIdentity(), null);
String fullName = userManager.getUserDisplayName(lockEntry.getOwner());
GenericMainController glc = createLockedMessageController(ureq, wControl, fullName);
return glc;
}else{
Controller controller = new IQDisplayController(resolver, type, secCallback, ureq, wControl);
//fxdiff BAKS-7 Resume function
OLATResourceableListeningWrapperController dwc = new OLATResourceableListeningWrapperController(ureq, wControl, res, controller, null, ureq.getIdentity());
return dwc;
}
}
private GenericMainController createLockedMessageController(UserRequest ureq, WindowControl wControl, String fullName) {
//wrap simple message into mainLayout
GenericMainController glc = new GenericMainController(ureq, wControl) {
@Override
public void init(UserRequest uureq) {
Panel empty = new Panel("empty");
setTranslator(Util.createPackageTranslator(this.getClass(), uureq.getLocale()));
Controller contentCtr = MessageUIFactory.createInfoMessage(uureq, getWindowControl(), translate("status.currently.locked.title"), translate("status.currently.locked", fullName));
listenTo(contentCtr); // auto dispose later
Component resComp = contentCtr.getInitialComponent();
LayoutMain3ColsController columnLayoutCtr = new LayoutMain3ColsController(uureq, getWindowControl(), empty, resComp, /*do not save no prefs*/null);
listenTo(columnLayoutCtr); // auto dispose later
putInitialPanel(columnLayoutCtr.getInitialComponent());
}
@Override
protected Controller handleOwnMenuTreeEvent(Object uobject, UserRequest uureq) {
//no menutree means no menu events.
return null;
}
};
glc.init(ureq);
return glc;
}
// --- end of controller creation
/**
*
* @param ai
* @param ureq
* @return
*/
public Document getResultsReporting(AssessmentInstance ai, Identity assessedIdentity, Locale locale) {
ResultsBuilder resB = new ResultsBuilder();
return resB.getResDoc(ai, locale, assessedIdentity);
}
/**
*
* @param identity
* @param type
* @param assessID
* @return
*/
public Document getResultsReportingFromFile(Identity identity, String type, long assessID) {
return FilePersister.retreiveResultsReporting(identity, type, assessID);
}
/**
*
* @param docResReporting
* @param locale
* @param detailed
* @return
*/
public String transformResultsReporting(Document docResReporting, Locale locale, int summaryType) {
switch (summaryType) {
case AssessmentInstance.SUMMARY_COMPACT: // Result summary without solutions
ResultsBuilder.stripDetails(docResReporting);
break;
case AssessmentInstance.SUMMARY_SECTION: // Section summary without solutions
ResultsBuilder.stripItemResults(docResReporting);
break;
case AssessmentInstance.SUMMARY_DETAILED:// Strip nothing
break;
default: // default => Strip nothing
break;
}
return LocalizedXSLTransformer.getInstance(locale).renderResults(docResReporting);
}
/**
* Extract item inputs from http request
*
* @param ureq The request to extract item responses from.
* @return ItemsInput
*/
public ItemsInput getItemsInput(UserRequest ureq) {
ItemsInput result = new ItemsInput();
Enumeration<String> params = ureq.getHttpReq().getParameterNames();
while (params.hasMoreElements()) {
String paramKey = params.nextElement();
StringTokenizer st = new StringTokenizer(paramKey, "§", false);
String value = ureq.getParameter(paramKey);
if (st.countTokens() == 4) {
String itemType = st.nextToken();
String itemIdent = st.nextToken();
String responseID = st.nextToken();
HttpItemInput itemInput = (HttpItemInput) result.getItemInput(itemIdent);
if (itemInput == null) {
itemInput = new HttpItemInput(itemIdent);
result.addItemInput(itemInput);
}
// 'dummy' type is used to make sure iteminput is constructed for
// all items. it does not provide any response data
if (itemType.equals("qti")) itemInput.putSingle(responseID, value);
}
// refactoring to new setFormDirty() javascript method sends now an additional param "olat_fosm" which has no tokens inside
// so assertExc. is useless.
//else {
// throw new AssertException ("not 4 tokens in form name: orig='"+paramKey+"'");
//}
//<input id="QTI_1098869464495" type="checkbox"
// name="qti§QTIEDIT:MCQ:1098869464490§1098869464492§1098869464495" ....
}
return result;
}
/**
* Create the QTIResults on the database for a given assessments,
* self-assessment or survey. These database entries can be used for
* statistical downloads.
*
*
* @param ai
* @param resId
* @param resDetail
* @param ureq
*/
public void persistResults(AssessmentInstance ai) {
AssessmentContext ac = ai.getAssessmentContext();
QTIResultSet qtiResultSet = new QTIResultSet();
qtiResultSet.setLastModified(new Date(System.currentTimeMillis()));
qtiResultSet.setOlatResource(ai.getCallingResId());
qtiResultSet.setOlatResourceDetail(ai.getCallingResDetail());
qtiResultSet.setRepositoryRef(ai.getRepositoryEntryKey());
qtiResultSet.setIdentity(ai.getAssessedIdentity());
qtiResultSet.setQtiType(ai.getType());
qtiResultSet.setAssessmentID(ai.getAssessID());
qtiResultSet.setDuration(new Long(ai.getAssessmentContext().getDuration()));
if (ai.isSurvey()){
qtiResultSet.setScore(0);
qtiResultSet.setIsPassed(true);
} else {
qtiResultSet.setScore(ac.getScore());
qtiResultSet.setIsPassed(ac.isPassed());
}
dbInstance.getCurrentEntityManager().persist(qtiResultSet);
// Loop over all sections in this assessment
int sccnt = ac.getSectionContextCount();
for (int i = 0; i < sccnt; i++) {
// Loop over all items in this section
SectionContext sc = ac.getSectionContext(i);
int iccnt = sc.getItemContextCount();
for (int j = 0; j < iccnt; j++) {
ItemContext ic = sc.getItemContext(j);
// Create new result item for this item
QTIResult qtiResult = new QTIResult();
qtiResult.setResultSet(qtiResultSet);
qtiResult.setItemIdent(ic.getIdent());
qtiResult.setDuration(new Long(ic.getTimeSpent()));
if (ai.isSurvey()) qtiResult.setScore(0);
else qtiResult.setScore(ic.getScore());
qtiResult.setTstamp(new Date(ic.getLatestAnswerTime()));
qtiResult.setLastModified(new Date(System.currentTimeMillis()));
qtiResult.setIp(ai.getRemoteAddr());
// Get user answers for this item
StringBuilder sb = new StringBuilder();
if (ic.getItemInput() == null) {} else {
ItemInput inp = ic.getItemInput();
if (inp.isEmpty()) {
sb.append("[]");
} else {
Map<String,List<String>> im = inp.getInputMap();
// Create answer block
Set<String> keys = im.keySet();
Iterator<String> iter = keys.iterator();
while (iter.hasNext()) {
String ident = iter.next();
sb.append(ident); // response_lid ident
sb.append("[");
List<String> answers = inp.getAsList(ident);
for (int y = 0; y < answers.size(); y++) {
sb.append("[");
String answer = answers.get(y);
// answer is referenced to response_label ident, if
// render_choice
// answer is userinput, if render_fib
answer = quoteSpecialQTIResultCharacters(answer);
sb.append(answer);
sb.append("]");
}
sb.append("]");
}
}
}
qtiResult.setAnswer(sb.toString());
// Persist result data in database
dbInstance.getCurrentEntityManager().persist(qtiResult);
}
}
}
/**
* Qotes special characters used by the QTIResult answer formatting. Special
* characters are '\', '[', ']', '\t', '\n', '\r', '\f', '\a' and '\e'
*
* @param string The string to be quoted
* @return The quoted string
*/
public String quoteSpecialQTIResultCharacters(String string) {
string = string.replaceAll("\\\\", "\\\\\\\\");
string = string.replaceAll("\\[", "\\\\[");
string = string.replaceAll("\\]", "\\\\]");
string = string.replaceAll("\\t", "\\\\t");
string = string.replaceAll("\\n", "\\\\n");
string = string.replaceAll("\\r", "\\\\r");
string = string.replaceAll("\\f", "\\\\f");
string = string.replaceAll("\\a", "\\\\a");
string = string.replaceAll("\\e", "\\\\e");
return string;
}
/**
* Unquotes special characters in the QTIResult answer texts.
*
* @see org.olat.modules.iq.IQManager#quoteSpecialQTIResultCharacters(String)
* @param string
* @return The unquoted sting
*/
public String unQuoteSpecialQTIResultCharacters(String string) {
string = string.replaceAll("\\\\[", "\\[");
string = string.replaceAll("\\\\]", "\\]");
string = string.replaceAll("\\\\t", "\\t");
string = string.replaceAll("\\\\n", "\\n");
string = string.replaceAll("\\\\r", "\\r");
string = string.replaceAll("\\\\f", "\\f");
string = string.replaceAll("\\\\a", "\\a");
string = string.replaceAll("\\\\e", "\\e");
string = string.replaceAll("\\\\\\\\", "\\\\");
return string;
}
/**
* Delete all qti.ser and qti-resreporting files.
* @see org.olat.user.UserDataDeletable#deleteUserData(org.olat.core.id.Identity)
*/
@Override
public void deleteUserData(Identity identity, String newDeletedUserName, File archivePath) {
FilePersister.deleteUserData(identity);
if(log.isDebug())
log.debug("Delete all qti.ser data and qti-resreporting data for identity=" + identity);
}
/**
* Returns null if no QTIResultSet found.
* @param identity
* @param olatResource
* @param olatResourceDetail
* @return Returns the last recorded QTIResultSet
*/
public QTIResultSet getLastResultSet(Identity identity, long olatResource, String olatResourceDetail) {
StringBuilder sb = new StringBuilder();
sb.append("select q from ").append(QTIResultSet.class.getName()).append(" q")
.append(" where q.identity.key=:identityKey and q.olatResource=:resourceId and q.olatResourceDetail=:resSubPath")
.append(" order by q.creationDate desc");
List<QTIResultSet> sets = dbInstance.getCurrentEntityManager().createQuery(sb.toString(), QTIResultSet.class)
.setParameter("identityKey", identity.getKey())
.setParameter("resourceId", new Long(olatResource))
.setParameter("resSubPath", olatResourceDetail)
.setMaxResults(1).getResultList();
if(sets.isEmpty()) {
return null;
}
return sets.get(0);
}
/**
* This should only be used as fallback solution if the assessmentID is not available via the AssessmentManager
* (migration of old tests)
* @param identity
* @param olatResource is the course id
* @param olatResourceDetail is the node id
* @return Returns the last assessmentID if at least a QTIResultSet was stored for the input variables, null otherwise.
*/
public Long getLastAssessmentID(Identity identity, long olatResource, String olatResourceDetail) {
QTIResultSet resultSet = getLastResultSet(identity, olatResource, olatResourceDetail);
if(resultSet!=null) {
return resultSet.getAssessmentID();
}
return null;
}
/**
* Get identities with exists qti.ser file.
* @param resourceableId
* @param ident
* @return
*/
public List<Identity> getIdentitiesWithQtiSerEntry(Long resourceableId, String ident) {
List<Identity> identities = new ArrayList<Identity>();
LocalFolderImpl item = new LocalFolderImpl(new File(FilePersister.getFullPathToCourseNodeDirectory(Long.toString(resourceableId), ident)));
if (VFSManager.exists(item)) {
for (VFSItem identityFolder : item.getItems()) {
Identity identity = BaseSecurityManager.getInstance().findIdentityByName(identityFolder.getName());
if (identity != null) identities.add(identity);
}
}
return identities;
}
/**
* Removes course node directory including qti.ser files of different users.
* @param resourceableId
* @param ident
* @return
*/
public VFSStatus removeQtiSerFiles(Long resourceableId, String ident) {
if (resourceableId == null || ident == null || ident.length() == 0) return VFSConstants.NO;
LocalFolderImpl item = new LocalFolderImpl(new File(FilePersister.getFullPathToCourseNodeDirectory(Long.toString(resourceableId), ident)));
if(item.canDelete().equals(VFSConstants.YES)) return item.delete();
return VFSConstants.NO;
}
}