diff --git a/src/main/java/org/olat/core/gui/components/progressbar/ProgressBarRenderer.java b/src/main/java/org/olat/core/gui/components/progressbar/ProgressBarRenderer.java index c2814ae3b01ef8dd6ddd1ba9c85ad608637c02b3..cb4a35944c9bd01862e2eb3f37541e30f6181120 100644 --- a/src/main/java/org/olat/core/gui/components/progressbar/ProgressBarRenderer.java +++ b/src/main/java/org/olat/core/gui/components/progressbar/ProgressBarRenderer.java @@ -69,7 +69,7 @@ public class ProgressBarRenderer extends DefaultComponentRenderer { .append("%", "px", ubar.isWidthInPercent()) .append(";\"><div class='progress-bar' style=\"width:") .append(Math.round(percent * ubar.getWidth() / 100)) - .append("px\" title=\"") + .append("%", "px", ubar.isWidthInPercent()).append("\" title=\"") .append(Math.round(percent)) .append("%\">"); if (renderLabels) { diff --git a/src/main/java/org/olat/core/gui/components/stack/BreadcrumbPanel.java b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbPanel.java index b39e624031f7077fe0bcb031913f3dd940b19aa6..f256b6914fc20eadf55a0242db06068924de2a0b 100644 --- a/src/main/java/org/olat/core/gui/components/stack/BreadcrumbPanel.java +++ b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbPanel.java @@ -29,6 +29,8 @@ import org.olat.core.gui.control.Controller; */ public interface BreadcrumbPanel extends StackedPanel { + public int size(); + /** * Dismiss all controller and replace the root * @param displayName diff --git a/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanel.java b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanel.java index 5d85bdf4363a96099fc10b66e178db4bad90ccbe..9b7a4e0aa6ae5d7bb3381611020ae3e3622589c0 100644 --- a/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanel.java +++ b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanel.java @@ -201,6 +201,11 @@ public class BreadcrumbedStackedPanel extends Panel implements StackedPanel, Bre BusinessControlFactory.getInstance().addToHistory(ureq, wControl); } + public int size() { + return stack == null ? 0 : stack.size(); + } + + @Override public Controller getRootController() { Controller controller = null; if(stack.size() > 0) { diff --git a/src/main/java/org/olat/course/assessment/ui/tool/IdentityListCourseNodeController.java b/src/main/java/org/olat/course/assessment/ui/tool/IdentityListCourseNodeController.java index 90d8e34cb477988d7f005e3fe5d96a6e0a26a28a..fb0aae73a9a616b0e84bfbec14682eca257b92e1 100644 --- a/src/main/java/org/olat/course/assessment/ui/tool/IdentityListCourseNodeController.java +++ b/src/main/java/org/olat/course/assessment/ui/tool/IdentityListCourseNodeController.java @@ -110,6 +110,7 @@ public class IdentityListCourseNodeController extends FormBasicController implem private final AssessmentToolContainer toolContainer; private IdentityListCourseNodeTableModel usersTableModel; + private List<Controller> toolsCtrl; private AssessedIdentityController currentIdentityCtrl; @Autowired @@ -289,6 +290,7 @@ public class IdentityListCourseNodeController extends FormBasicController implem AssessableCourseNode acn = (AssessableCourseNode)courseNode; ICourse course = CourseFactory.loadCourse(courseEntry); AssessmentToolOptions options = new AssessmentToolOptions(); + options.setAdmin(assessmentCallback.isAdmin()); if(group == null) { options.setIdentities(assessedIdentities); fillAlternativeToAssessableIdentityList(options); @@ -310,7 +312,7 @@ public class IdentityListCourseNodeController extends FormBasicController implem } } } - + toolsCtrl = tools; } flc.contextPut("toolCmpNames", toolCmpNames); } @@ -414,6 +416,10 @@ public class IdentityListCourseNodeController extends FormBasicController implem } else if(event == Event.CANCELLED_EVENT) { stackPanel.popController(currentIdentityCtrl); } + } else if(toolsCtrl != null && toolsCtrl.contains(source)) { + if(event == Event.CHANGED_EVENT) { + updateModel(ureq, null, null, null); + } } super.event(ureq, source, event); } diff --git a/src/main/java/org/olat/course/nodes/AssessmentToolOptions.java b/src/main/java/org/olat/course/nodes/AssessmentToolOptions.java index 6010ed80def0d64cca7e65061b4dd749fcf5ea37..da31805994e51151ae7352a35541adbbb2980af3 100644 --- a/src/main/java/org/olat/course/nodes/AssessmentToolOptions.java +++ b/src/main/java/org/olat/course/nodes/AssessmentToolOptions.java @@ -33,10 +33,23 @@ import org.olat.group.BusinessGroup; */ public class AssessmentToolOptions { + private boolean admin; private BusinessGroup group; private List<Identity> identities; private AlternativeToIdentities alternativeToIdentities; + public AssessmentToolOptions() { + // + } + + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + public BusinessGroup getGroup() { return group; } diff --git a/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java b/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java index f19f4aa1ff0eff1b609959157496bcfcfd176f22..2d3677c91341baa244c9940309a3416b9b1b0974 100644 --- a/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java +++ b/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java @@ -85,8 +85,10 @@ import org.olat.ims.qti.statistics.QTIStatisticSearchParams; import org.olat.ims.qti.statistics.QTIType; import org.olat.ims.qti.statistics.ui.QTI12PullTestsToolController; import org.olat.ims.qti.statistics.ui.QTI12StatisticsToolController; +import org.olat.ims.qti21.manager.archive.QTI21ArchiveFormat; import org.olat.ims.qti21.model.QTI21StatisticSearchParams; import org.olat.ims.qti21.ui.QTI21AssessmentDetailsController; +import org.olat.ims.qti21.ui.QTI21ResetToolController; import org.olat.ims.qti21.ui.statistics.QTI21StatisticResourceResult; import org.olat.ims.qti21.ui.statistics.QTI21StatisticsToolController; import org.olat.modules.ModuleConfiguration; @@ -205,12 +207,14 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe RepositoryEntry qtiTestEntry = getReferencedRepositoryEntry(); if(ImsQTI21Resource.TYPE_NAME.equals(qtiTestEntry.getOlatResource().getResourceableTypeName())) { tools.add(new QTI21StatisticsToolController(ureq, wControl, stackPanel, courseEnv, options, this)); - //TODO qti implements the pull tests + if(options.isAdmin()) { + tools.add(new QTI21ResetToolController(ureq, wControl, courseEnv, options, this)); + } } else { tools.add(new QTI12StatisticsToolController(ureq, wControl, stackPanel, courseEnv, options, this)); if(options.getGroup() == null && options.getIdentities() != null && options.getIdentities().size() > 0) { for(Identity assessedIdentity:options.getIdentities()) { - if(isTestRunning(assessedIdentity, courseEnv)) { + if(isQTI12TestRunning(assessedIdentity, courseEnv)) { tools.add(new QTI12PullTestsToolController(ureq, wControl, courseEnv, options, this)); break; } @@ -220,7 +224,7 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe return tools; } - public boolean isTestRunning(Identity assessedIdentity, CourseEnvironment courseEnv) { + public boolean isQTI12TestRunning(Identity assessedIdentity, CourseEnvironment courseEnv) { String resourcePath = courseEnv.getCourseResourceableId() + File.separator + getIdent(); FilePersister qtiPersister = new FilePersister(assessedIdentity, resourcePath); return qtiPersister.exists(); @@ -585,6 +589,11 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe OnyxExportManager.getInstance().exportResults(results, exportStream, this); } return true; + } else if(ImsQTI21Resource.TYPE_NAME.equals(re.getOlatResource().getResourceableTypeName())) { + QTI21ArchiveFormat qaf = new QTI21ArchiveFormat(locale); + RepositoryEntry courseEntry = course.getCourseEnvironment().getCourseGroupManager().getCourseEntry(); + qaf.export(courseEntry, getIdent(), re, exportStream); + return true; } else { String shortTitle = getShortTitle(); QTIExportManager qem = QTIExportManager.getInstance(); diff --git a/src/main/java/org/olat/course/run/CourseRuntimeController.java b/src/main/java/org/olat/course/run/CourseRuntimeController.java index af3e3eb22d0f358985b8c5b23d2d490ea2f1a7d2..1bf752c27317b12dd7c34c47c97e7a156c3f853d 100644 --- a/src/main/java/org/olat/course/run/CourseRuntimeController.java +++ b/src/main/java/org/olat/course/run/CourseRuntimeController.java @@ -390,6 +390,7 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im initTools(toolsDropdown, course, uce); initSettingsTools(settingsDropdown); initEditionTools(settingsDropdown); + initDeleteTools(settingsDropdown, true); } initToolsMyCourse(course, uce); initGeneralTools(course); @@ -546,7 +547,40 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im } } } - + + @Override + protected void initDeleteTools(Dropdown settingsDropdown, boolean needSpacer) { + RepositoryEntry re = getRepositoryEntry(); + boolean closeManged = RepositoryEntryManagedFlag.isManaged(re, RepositoryEntryManagedFlag.close); + + if(reSecurity.isEntryAdmin()) { + boolean deleteManaged = RepositoryEntryManagedFlag.isManaged(re, RepositoryEntryManagedFlag.delete); + if(settingsDropdown.size() > 0 && !deleteManaged) { + settingsDropdown.addComponent(new Spacer("close-delete")); + } + + if(!closeManged || !deleteManaged) { + // If a resource is closable (currently only course) and + // deletable (currently all resources) we offer those two + // actions in a separate page, unless both are managed + // operations. In that case we don't show anything at all. + // If only one of the two actions are managed, we go to the + // separate page as well and show only the relevant action + // there. + lifeCycleChangeLink = LinkFactory.createToolLink("lifeCycleChange", translate("details.lifecycle.change"), this, "o_icon o_icon-fw o_icon_lifecycle"); + settingsDropdown.addComponent(lifeCycleChangeLink); + } else { + if(!deleteManaged) { + String type = translate(handler.getSupportedType()); + String deleteTitle = translate("details.delete.alt", new String[]{ type }); + deleteLink = LinkFactory.createToolLink("delete", deleteTitle, this, "o_icon o_icon-fw o_icon_delete_item"); + deleteLink.setElementCssClass("o_sel_repo_close"); + settingsDropdown.addComponent(deleteLink); + } + } + } + } + private void initToolsMyCourse(ICourse course, UserCourseEnvironmentImpl uce) { boolean assessmentLock = isAssessmentLock(); diff --git a/src/main/java/org/olat/course/statistic/StatisticResourceNode.java b/src/main/java/org/olat/course/statistic/StatisticResourceNode.java index 05d2dcfe405c797876d57f4015fb4af4d66a4d41..b146d4c87b54e3f14dccf3b2cf7a2fac2527c309 100644 --- a/src/main/java/org/olat/course/statistic/StatisticResourceNode.java +++ b/src/main/java/org/olat/course/statistic/StatisticResourceNode.java @@ -29,7 +29,7 @@ import org.olat.course.nodes.CourseNodeFactory; * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ -public class StatisticResourceNode extends GenericTreeNode { +public class StatisticResourceNode extends GenericTreeNode { private static final long serialVersionUID = -1528483744004133623L; private final CourseNode courseNode; private final StatisticResourceResult result; diff --git a/src/main/java/org/olat/ims/qti/statistics/ui/QTI12PullTestsToolController.java b/src/main/java/org/olat/ims/qti/statistics/ui/QTI12PullTestsToolController.java index 9f3aecb86e7cf73d2a00b05ba597770c3f7d4fe6..730fdd97d839c7805e3f64365f443f8c42b509fe 100644 --- a/src/main/java/org/olat/ims/qti/statistics/ui/QTI12PullTestsToolController.java +++ b/src/main/java/org/olat/ims/qti/statistics/ui/QTI12PullTestsToolController.java @@ -127,7 +127,7 @@ public class QTI12PullTestsToolController extends BasicController implements Act int count = 0; StringBuilder fullnames = new StringBuilder(256); for(Identity assessedIdentity:assessedIdentities) { - if(courseNode.isTestRunning(assessedIdentity, courseEnv)) { + if(courseNode.isQTI12TestRunning(assessedIdentity, courseEnv)) { if(fullnames.length() > 0) fullnames.append(", "); String name = userManager.getUserDisplayName(assessedIdentity); if(StringHelper.containsNonWhitespace(name)) { @@ -153,7 +153,7 @@ public class QTI12PullTestsToolController extends BasicController implements Act private void doRetrieveTests() { ICourse course = CourseFactory.loadCourse(courseEnv.getCourseResourceableId()); for(Identity assessedIdentity:assessedIdentities) { - if(courseNode.isTestRunning(assessedIdentity, courseEnv)) { + if(courseNode.isQTI12TestRunning(assessedIdentity, courseEnv)) { IQRetrievedEvent retrieveEvent = new IQRetrievedEvent(assessedIdentity, courseEnv.getCourseResourceableId(), courseNode.getIdent()); CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(retrieveEvent, retrieveEvent); retrieveTest(assessedIdentity, course); diff --git a/src/main/java/org/olat/ims/qti21/QTI21Service.java b/src/main/java/org/olat/ims/qti21/QTI21Service.java index 4e0c9408e0c9fa19269dba2ba6f99b74c6a604cb..f349049e6821e4d715c02786a9fe441a9875c48b 100644 --- a/src/main/java/org/olat/ims/qti21/QTI21Service.java +++ b/src/main/java/org/olat/ims/qti21/QTI21Service.java @@ -114,6 +114,16 @@ public interface QTI21Service { */ public boolean needManualCorrection(ResolvedAssessmentTest resolvedAssessmentTest); + /** + * + * @param identities + * @param testEntry + * @param entry + * @param subIdent + * @return + */ + public boolean deleteAssessmentTestSession(List<Identity> identities, RepositoryEntryRef testEntry, RepositoryEntryRef entry, String subIdent); + /** * Remove all test sessions in author mode, e.g. after an assessment test * was changed. diff --git a/src/main/java/org/olat/ims/qti21/manager/AssessmentResponseDAO.java b/src/main/java/org/olat/ims/qti21/manager/AssessmentResponseDAO.java index f590118df1126753f59e8b15464e52933fc00cc6..3054016fbdbaec43061a7d7d10a33dab0888f949 100644 --- a/src/main/java/org/olat/ims/qti21/manager/AssessmentResponseDAO.java +++ b/src/main/java/org/olat/ims/qti21/manager/AssessmentResponseDAO.java @@ -23,7 +23,10 @@ import java.util.Collection; import java.util.Date; import java.util.List; +import javax.persistence.TypedQuery; + import org.olat.core.commons.persistence.DB; +import org.olat.core.util.StringHelper; import org.olat.ims.qti21.AssessmentItemSession; import org.olat.ims.qti21.AssessmentResponse; import org.olat.ims.qti21.AssessmentTestSession; @@ -120,20 +123,27 @@ public class AssessmentResponseDAO { .append(" inner join fetch testSession.assessmentEntry assessmentEntry") .append(" inner join assessmentEntry.identity as ident") .append(" inner join ident.user as usr") - .append(" where testSession.repositoryEntry.key=:repoEntryKey") - .append(" and testSession.testEntry.key=:testEntryKey") - .append(" and testSession.subIdent=:subIdent") + .append(" where testSession.testEntry.key=:testEntryKey") .append(" and testSession.terminationTime is not null"); + if(courseEntry != null) { + sb.append(" and testSession.repositoryEntry.key=:repoEntryKey"); + } + if(StringHelper.containsNonWhitespace(subIdent)) { + sb.append(" and testSession.subIdent=:subIdent"); + } //need to be anonymized sb.append(" order by usr.lastName, testSession.key, itemSession.key"); - return dbInstance.getCurrentEntityManager() + TypedQuery<AssessmentResponse> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), AssessmentResponse.class) - .setParameter("repoEntryKey", courseEntry.getKey()) - .setParameter("testEntryKey", testEntry.getKey()) - .setParameter("subIdent", subIdent) - .getResultList(); + .setParameter("testEntryKey", testEntry.getKey()); + if(courseEntry != null) { + query.setParameter("repoEntryKey", courseEntry.getKey()); + } + if(StringHelper.containsNonWhitespace(subIdent)) { + query.setParameter("subIdent", subIdent); + } + return query.getResultList(); } - } diff --git a/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java b/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java index 1340cfbaaac4d006885824dbb37a6002ba1bb6ed..1d739d2d460fcccc0dd09a068539c5f8c9577d56 100644 --- a/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java +++ b/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java @@ -117,6 +117,39 @@ public class AssessmentTestSessionDAO { return lastSessions == null || lastSessions.isEmpty() ? null : lastSessions.get(0); } + public List<AssessmentTestSession> getTestSessions(RepositoryEntryRef testEntry, + RepositoryEntryRef entry, String subIdent, IdentityRef identity) { + + StringBuilder sb = new StringBuilder(); + sb.append("select session from qtiassessmenttestsession session") + .append(" left join fetch session.assessmentEntry asEntry") + .append(" where session.testEntry.key=:testEntryKey and session.identity.key=:identityKey"); + if(entry != null) { + sb.append(" and session.repositoryEntry.key=:courseEntryKey"); + } else { + sb.append(" and session.repositoryEntry.key is null"); + } + + if(subIdent != null) { + sb.append(" and session.subIdent=:courseSubIdent"); + } else { + sb.append(" and session.subIdent is null"); + } + + TypedQuery<AssessmentTestSession> query = dbInstance.getCurrentEntityManager() + .createQuery(sb.toString(), AssessmentTestSession.class) + .setParameter("testEntryKey", testEntry.getKey()) + .setParameter("identityKey", identity.getKey()); + if(entry != null) { + query.setParameter("courseEntryKey", entry.getKey()); + } + if(subIdent != null) { + query.setParameter("courseSubIdent", subIdent); + } + + return query.getResultList(); + } + /** * Create a folder for a session in bcroot. * diff --git a/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java b/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java index 1c8fead8fa1ed35a3067b2082066fbad805bef75..e2715fba17faeb7f16bf44fb8476ce4106c6c862 100644 --- a/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java +++ b/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java @@ -24,19 +24,26 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.DoubleAdder; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.AbstractEntry; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.NumericalEntry; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntry; 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.Interaction; -import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.MapEntry; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.TextEntryInteraction; import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; import uk.ac.ed.ph.jqtiplus.node.shared.FieldValue; import uk.ac.ed.ph.jqtiplus.types.Identifier; +import uk.ac.ed.ph.jqtiplus.value.BaseType; import uk.ac.ed.ph.jqtiplus.value.Cardinality; import uk.ac.ed.ph.jqtiplus.value.DirectedPairValue; import uk.ac.ed.ph.jqtiplus.value.IdentifierValue; @@ -46,7 +53,6 @@ import uk.ac.ed.ph.jqtiplus.value.OrderedValue; import uk.ac.ed.ph.jqtiplus.value.PairValue; import uk.ac.ed.ph.jqtiplus.value.PointValue; import uk.ac.ed.ph.jqtiplus.value.SingleValue; -import uk.ac.ed.ph.jqtiplus.value.StringValue; import uk.ac.ed.ph.jqtiplus.value.Value; /** @@ -323,53 +329,17 @@ public class CorrectResponsesUtil { return correctAnswers; } - public static final TextEntry getCorrectTextResponses(AssessmentItem assessmentItem, Interaction interaction) { + public static final AbstractEntry getCorrectTextResponses(AssessmentItem assessmentItem, TextEntryInteraction interaction) { ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(interaction.getResponseIdentifier()); - - boolean caseSensitive = true; - 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(); - alternatives.add(value); - } - - caseSensitive &= mapEntry.getCaseSensitive(); - } - return new TextEntry(alternatives, caseSensitive); - } - - public static class TextEntry { - - private boolean caseSensitive; - private List<String> alternatives; - - public TextEntry(List<String> alternatives, boolean caseSensitive) { - this.alternatives = alternatives; - this.caseSensitive = caseSensitive; - } - - public boolean isCaseSensitive() { - return caseSensitive; - } - - public List<String> getAlternatives() { - return alternatives; - } - - public boolean isCorrect(String response) { - for(String alternative:alternatives) { - if(caseSensitive) { - if(alternative.equals(response)) { - return true; - } - } else if(alternative.equalsIgnoreCase(response)) { - return true; - } - } - return false; + if(responseDeclaration.hasBaseType(BaseType.STRING) && responseDeclaration.hasCardinality(Cardinality.SINGLE)) { + TextEntry textEntry = new TextEntry(interaction); + FIBAssessmentItemBuilder.extractTextEntrySettingsFromResponseDeclaration(textEntry, responseDeclaration, new AtomicInteger(), new DoubleAdder()); + return textEntry; + } else if(responseDeclaration.hasBaseType(BaseType.FLOAT) && responseDeclaration.hasCardinality(Cardinality.SINGLE)) { + NumericalEntry numericalEntry = new NumericalEntry(interaction); + FIBAssessmentItemBuilder.extractNumericalEntrySettings(assessmentItem, numericalEntry, responseDeclaration, new AtomicInteger(), new DoubleAdder()); + return numericalEntry; } + return null; } } diff --git a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java index 22d604e17462027142775edd912905f8c233b6f7..860401d7a615595fc4a14701416b1f2768c4234f 100644 --- a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java +++ b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java @@ -32,9 +32,11 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -77,6 +79,7 @@ import org.olat.ims.qti21.model.audit.CandidateEvent; import org.olat.ims.qti21.model.audit.CandidateItemEventType; import org.olat.ims.qti21.model.audit.CandidateTestEventType; import org.olat.modules.assessment.AssessmentEntry; +import org.olat.modules.assessment.manager.AssessmentEntryDAO; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryEntryRef; import org.springframework.beans.factory.DisposableBean; @@ -165,6 +168,8 @@ public class QTI21ServiceImpl implements QTI21Service, InitializingBean, Disposa @Autowired private AssessmentTestMarksDAO testMarksDao; @Autowired + private AssessmentEntryDAO assessmentEntryDao; + @Autowired private QTI21Module qtiModule; @Autowired private CoordinatorManager coordinatorManager; @@ -397,6 +402,27 @@ public class QTI21ServiceImpl implements QTI21Service, InitializingBean, Disposa } }); } + + @Override + public boolean deleteAssessmentTestSession(List<Identity> identities, RepositoryEntryRef testEntry, RepositoryEntryRef entry, String subIdent) { + Set<AssessmentEntry> entries = new HashSet<>(); + for(Identity identity:identities) { + List<AssessmentTestSession> sessions = testSessionDao.getTestSessions(testEntry, entry, subIdent, identity); + for(AssessmentTestSession session:sessions) { + if(session.getAssessmentEntry() != null) { + entries.add(session.getAssessmentEntry()); + } + File fileStorage = testSessionDao.getSessionStorage(session); + testSessionDao.deleteTestSession(session); + FileUtils.deleteDirsAndFiles(fileStorage, true, true); + } + } + + for(AssessmentEntry assessmentEntry:entries) { + assessmentEntryDao.resetAssessmentEntry(assessmentEntry); + } + return true; + } @Override public boolean deleteAuthorAssessmentTestSession(RepositoryEntryRef testEntry) { diff --git a/src/main/java/org/olat/ims/qti21/manager/archive/QTI21ArchiveFormat.java b/src/main/java/org/olat/ims/qti21/manager/archive/QTI21ArchiveFormat.java index 467014b488f43247df334dfe5e24c97b9966165d..d1f5916e1c37525ee40c6156489fb2df6bf05f5f 100644 --- a/src/main/java/org/olat/ims/qti21/manager/archive/QTI21ArchiveFormat.java +++ b/src/main/java/org/olat/ims/qti21/manager/archive/QTI21ArchiveFormat.java @@ -20,6 +20,7 @@ package org.olat.ims.qti21.manager.archive; import java.io.File; +import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; @@ -28,7 +29,10 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.IOUtils; import org.olat.core.CoreSpringFactory; import org.olat.core.gui.media.MediaResource; import org.olat.core.gui.translator.Translator; @@ -40,6 +44,7 @@ import org.olat.core.logging.Tracing; import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.core.util.Util; +import org.olat.core.util.io.ShieldOutputStream; import org.olat.core.util.openxml.OpenXMLWorkbook; import org.olat.core.util.openxml.OpenXMLWorkbookResource; import org.olat.core.util.openxml.OpenXMLWorksheet; @@ -59,7 +64,6 @@ import org.olat.ims.qti21.manager.QTI21ServiceImpl; import org.olat.ims.qti21.manager.archive.interactions.AssociateInteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.ChoiceInteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.DefaultInteractionArchive; -import org.olat.ims.qti21.manager.archive.interactions.NoOutputInteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.ExtendedTextInteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.GapMatchInteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.GraphicAssociateInteractionArchive; @@ -71,6 +75,7 @@ import org.olat.ims.qti21.manager.archive.interactions.InlineChoiceInteractionAr import org.olat.ims.qti21.manager.archive.interactions.InteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.MatchInteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.MediaInteractionArchive; +import org.olat.ims.qti21.manager.archive.interactions.NoOutputInteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.OrderInteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.PositionObjectInteractionArchive; import org.olat.ims.qti21.manager.archive.interactions.SelectPointInteractionArchive; @@ -173,9 +178,55 @@ public class QTI21ArchiveFormat { public boolean hasResults(RepositoryEntry courseEntry, String subIdent, RepositoryEntry testEntry) { return responseDao.hasResponses(courseEntry, subIdent, testEntry); } + + public void export(RepositoryEntry courseEntry, String subIdent, RepositoryEntry testEntry, ZipOutputStream exportStream) { + FileResourceManager frm = FileResourceManager.getInstance(); + File unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource()); + resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, false); + + CourseNode courseNode = CourseFactory.loadCourse(courseEntry).getRunStructure().getNode(subIdent); + String label = courseNode.getType() + "_" + + StringHelper.transformDisplayNameToFileSystemName(courseNode.getShortName()) + + "_" + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis())) + + ".xlsx"; + + export(courseEntry, subIdent, testEntry, label, exportStream); + } - public MediaResource export(RepositoryEntry courseEntry, String subIdent, RepositoryEntry testEntry) { + public void export(RepositoryEntry testEntry, ZipOutputStream exportStream) { + FileResourceManager frm = FileResourceManager.getInstance(); + File unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource()); + resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, false); + String archiveName = "qti21test_" + + StringHelper.transformDisplayNameToFileSystemName(testEntry.getDisplayname()) + + "_" + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis())) + ".xlsx"; + export(null, null, testEntry, archiveName, exportStream); + } + + private void export(RepositoryEntry courseEntry, String subIdent, RepositoryEntry testEntry, String filename, ZipOutputStream exportStream) { + //content + final List<AssessmentResponse> responses = responseDao.getResponse(courseEntry, subIdent, testEntry); + try { + exportStream.putNextEntry(new ZipEntry(filename)); + OpenXMLWorkbook workbook = new OpenXMLWorkbook(new ShieldOutputStream(exportStream), 1); + + //headers + OpenXMLWorksheet exportSheet = workbook.nextWorksheet(); + exportSheet.setHeaderRows(2); + writeHeaders_1(exportSheet, workbook); + writeHeaders_2(exportSheet, workbook); + writeData(responses, exportSheet, workbook); + + IOUtils.closeQuietly(workbook); + + exportStream.closeEntry(); + } catch (IOException e) { + log.error("", e); + } + } + + public MediaResource export(RepositoryEntry courseEntry, String subIdent, RepositoryEntry testEntry) { FileResourceManager frm = FileResourceManager.getInstance(); File unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource()); resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, false); diff --git a/src/main/java/org/olat/ims/qti21/manager/archive/interactions/TextEntryInteractionArchive.java b/src/main/java/org/olat/ims/qti21/manager/archive/interactions/TextEntryInteractionArchive.java index b602bae321fa7cea34bd7957fa32c760d9dc3225..cd6ed2ec305708fbb4bf6aa6846e60ad27a151d7 100644 --- a/src/main/java/org/olat/ims/qti21/manager/archive/interactions/TextEntryInteractionArchive.java +++ b/src/main/java/org/olat/ims/qti21/manager/archive/interactions/TextEntryInteractionArchive.java @@ -24,7 +24,7 @@ import org.olat.core.util.openxml.OpenXMLWorkbook; import org.olat.core.util.openxml.OpenXMLWorksheet.Row; import org.olat.ims.qti21.AssessmentResponse; import org.olat.ims.qti21.manager.CorrectResponsesUtil; -import org.olat.ims.qti21.manager.CorrectResponsesUtil.TextEntry; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.AbstractEntry; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; @@ -52,10 +52,10 @@ public class TextEntryInteractionArchive extends DefaultInteractionArchive { String stringuifiedResponses = response == null ? null : response.getStringuifiedResponse(); if(StringHelper.containsNonWhitespace(stringuifiedResponses)) { TextEntryInteraction textEntryInteraction = (TextEntryInteraction)interaction; - TextEntry correctAnswers = CorrectResponsesUtil.getCorrectTextResponses(item, textEntryInteraction); + AbstractEntry correctAnswers = CorrectResponsesUtil.getCorrectTextResponses(item, textEntryInteraction); stringuifiedResponses = CorrectResponsesUtil.stripResponse(stringuifiedResponses); - - boolean correct = correctAnswers.isCorrect(stringuifiedResponses); + + boolean correct = correctAnswers.match(stringuifiedResponses); if(correct) { dataRow.addCell(col++, stringuifiedResponses, workbook.getStyles().getCorrectStyle()); } else { diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java index def9642a2a42c875df9e8f7dc9896c40d5875fab..8d4b0400d2ed6b594dda6a727001467e6757e409 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java @@ -854,6 +854,8 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { public void setScore(Double score) { this.score = score; } + + public abstract boolean match(String response); } public static class NumericalEntry extends AbstractEntry { @@ -903,6 +905,19 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { public void setToleranceMode(ToleranceMode toleranceMode) { this.toleranceMode = toleranceMode; } + + @Override + public boolean match(String response) { + try { + double firstNumber = Double.parseDouble(response); + return toleranceMode.isEqual(firstNumber, solution, + lowerTolerance, upperTolerance, + true, true); + } catch (Exception e) { + log.error("", e); + return false; + } + } } public static class TextEntry extends AbstractEntry { @@ -1001,6 +1016,37 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { } } } + + /** + * Quick method to find if a string match the correct responses of + * the text entry. + * + * @param response + * @return + */ + public boolean match(String response) { + if(match(response, solution)) { + return true; + } + + for(TextEntryAlternative textEntryAlternative:alternatives) { + if(match(response, textEntryAlternative.getAlternative())) { + return true; + } + } + return false; + } + + private boolean match(String response, String alternative) { + if(caseSensitive) { + if(alternative.equals(response)) { + return true; + } + } else if(alternative.equalsIgnoreCase(response)) { + return true; + } + return false; + } } public static class TextEntryAlternative { diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java index dbc9d39807925c2cb2b5258a1a5c0c1e5306e12a..b41d62c12bb72c6d979ceda6030a5b8b85a85f76 100644 --- a/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java +++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java @@ -183,7 +183,7 @@ public class AssessmentResultController extends FormBasicController { Results r = new Results(false, type.getCssClass()); r.setTitle(node.getSectionPartTitle()); - r.setSessionStatus(this.translate("")); + r.setSessionStatus("");//init ItemSessionState sessionState = testSessionState.getItemSessionStates().get(testPlanNodeKey); if(sessionState != null) { diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java index 0b2812917eb65c0f1abf81ef8899d3aad5703e65..082d46615503ce370e4af744583fc458ddef253d 100644 --- a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java +++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java @@ -1413,7 +1413,7 @@ public class AssessmentTestDisplayController extends BasicController implements resultsVisible = true; } - if(testSessionController.findNextEnterableTestPart() == null) { + if(testSessionController.getTestSessionState().isEnded() || testSessionController.findNextEnterableTestPart() == null) { closeTestButton.setI18nKey("assessment.test.close.test"); } else { closeTestButton.setI18nKey("assessment.test.close.testpart"); diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21ResetToolController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21ResetToolController.java new file mode 100644 index 0000000000000000000000000000000000000000..e40416fecdd25764a227d1470eb15b9122e3bfb7 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/QTI21ResetToolController.java @@ -0,0 +1,171 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <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 the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <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> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.ui; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.zip.ZipOutputStream; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.Component; +import org.olat.core.gui.components.link.Link; +import org.olat.core.gui.components.link.LinkFactory; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.Event; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.gui.control.controller.BasicController; +import org.olat.core.gui.control.generic.modal.DialogBoxController; +import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory; +import org.olat.core.id.Identity; +import org.olat.core.id.IdentityEnvironment; +import org.olat.core.id.Roles; +import org.olat.core.util.Formatter; +import org.olat.core.util.StringHelper; +import org.olat.course.CourseFactory; +import org.olat.course.ICourse; +import org.olat.course.nodes.ArchiveOptions; +import org.olat.course.nodes.AssessmentToolOptions; +import org.olat.course.nodes.IQTESTCourseNode; +import org.olat.course.nodes.QTICourseNode; +import org.olat.course.run.environment.CourseEnvironment; +import org.olat.course.run.scoring.ScoreEvaluation; +import org.olat.course.run.userview.UserCourseEnvironment; +import org.olat.course.run.userview.UserCourseEnvironmentImpl; +import org.olat.group.BusinessGroupService; +import org.olat.ims.qti21.QTI21Service; +import org.olat.repository.RepositoryEntry; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * + * Initial date: 08.08.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QTI21ResetToolController extends BasicController { + + private final Roles studentRoles = new Roles(false, false, false, false, false, false, false, false); + private final Link resetButton; + + private DialogBoxController confirmResetDialog; + + private final QTICourseNode courseNode; + private final CourseEnvironment courseEnv; + private final AssessmentToolOptions asOptions; + + @Autowired + private QTI21Service qtiService; + @Autowired + private BusinessGroupService businessGroupService; + + public QTI21ResetToolController(UserRequest ureq, WindowControl wControl, + CourseEnvironment courseEnv, AssessmentToolOptions asOptions, QTICourseNode courseNode) { + super(ureq, wControl); + this.courseNode = courseNode; + this.courseEnv = courseEnv; + this.asOptions = asOptions; + + resetButton = LinkFactory.createButton("reset.test.data.title", null, this); + resetButton.setTranslator(getTranslator()); + putInitialPanel(resetButton); + getInitialComponent().setSpanAsDomReplaceable(true); // override to wrap panel as span to not break link layout + } + + @Override + protected void doDispose() { + // + } + + @Override + protected void event(UserRequest ureq, Component source, Event event) { + if(resetButton == source) { + doConfirmReset(ureq); + } + } + + @Override + protected void event(UserRequest ureq, Controller source, Event event) { + if(confirmResetDialog == source) { + if(DialogBoxUIFactory.isOkEvent(event) || DialogBoxUIFactory.isYesEvent(event)) { + doReset(ureq); + } + } + super.event(ureq, source, event); + } + + private void doConfirmReset(UserRequest ureq) { + String title = translate("reset.test.data.title"); + String text = translate("reset.test.data.text"); + confirmResetDialog = activateOkCancelDialog(ureq, title, text, confirmResetDialog); + } + + private void doReset(UserRequest ureq) { + List<Identity> identities; + + ArchiveOptions options = new ArchiveOptions(); + if(asOptions.getGroup() == null) { + identities = asOptions.getIdentities(); + options.setIdentities(identities); + } else { + identities = businessGroupService.getMembers(asOptions.getGroup()); + options.setGroup(asOptions.getGroup()); + } + + RepositoryEntry testEntry = courseNode.getReferencedRepositoryEntry(); + RepositoryEntry courseEntry = courseEnv.getCourseGroupManager().getCourseEntry(); + + if(courseNode instanceof IQTESTCourseNode) { + IQTESTCourseNode testCourseNode = (IQTESTCourseNode)courseNode; + + ICourse course = CourseFactory.loadCourse(courseEntry); + archiveData(course, options); + + qtiService.deleteAssessmentTestSession(identities, testEntry, courseEntry, courseNode.getIdent()); + for(Identity identity:identities) { + ScoreEvaluation scoreEval = new ScoreEvaluation(null, null); + + IdentityEnvironment ienv = new IdentityEnvironment(identity, studentRoles); + UserCourseEnvironment uce = new UserCourseEnvironmentImpl(ienv, courseEnv); + testCourseNode.updateUserScoreEvaluation(scoreEval, uce, getIdentity(), false); + } + } + + fireEvent(ureq, Event.CHANGED_EVENT); + } + + private void archiveData(ICourse course, ArchiveOptions options) { + File exportDirectory = CourseFactory.getOrCreateDataExportDirectory(getIdentity(), course.getCourseTitle()); + String archiveName = courseNode.getType() + "_" + + StringHelper.transformDisplayNameToFileSystemName(courseNode.getShortName()) + + "_" + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis())) + ".zip"; + + File exportFile = new File(exportDirectory, archiveName); + try(FileOutputStream fileStream = new FileOutputStream(exportFile); + ZipOutputStream exportStream = new ZipOutputStream(fileStream)) { + + courseNode.archiveNodeData(getLocale(), course, options, exportStream, "UTF-8"); + } catch (IOException e) { + logError("", e); + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeController.java index b96bf821f0d4840447ca158c6d2f213a6c0cf846..e79d3b1feb64dccddd92e911e750131ab814a049 100644 --- a/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeController.java +++ b/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeController.java @@ -20,7 +20,15 @@ package org.olat.ims.qti21.ui; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Date; +import java.util.List; +import java.util.zip.ZipOutputStream; +import org.olat.core.commons.modules.bc.FolderConfig; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.dropdown.Dropdown; @@ -32,15 +40,21 @@ import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.generic.dtabs.Activateable2; +import org.olat.core.gui.control.generic.modal.DialogBoxController; +import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory; +import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.activity.ThreadLocalUserActivityLogger; +import org.olat.core.util.Formatter; +import org.olat.core.util.StringHelper; import org.olat.core.util.resource.OresHelper; +import org.olat.course.nodes.AssessmentToolOptions; import org.olat.fileresource.FileResourceManager; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.QTI21Service; +import org.olat.ims.qti21.manager.archive.QTI21ArchiveFormat; import org.olat.ims.qti21.model.xml.QtiNodesExtractor; import org.olat.ims.qti21.ui.editor.AssessmentTestComposerController; -import org.olat.ims.qti21.ui.statistics.QTI21AssessmentTestStatisticsController; import org.olat.modules.assessment.ui.AssessableResource; import org.olat.modules.assessment.ui.AssessmentToolController; import org.olat.modules.assessment.ui.AssessmentToolSecurityCallback; @@ -62,11 +76,14 @@ import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest; */ public class QTI21RuntimeController extends RepositoryEntryRuntimeController { - private Link assessmentLink, testStatisticLink, qtiOptionsLink; + private Link assessmentLink, testStatisticLink, qtiOptionsLink, resetDataLink; + private DialogBoxController confirmResetDialog; private QTI21DeliveryOptionsController optionsCtrl; private AssessmentToolController assessmentToolCtrl; - private QTI21AssessmentTestStatisticsController statsToolCtr; + private QTI21RuntimeStatisticsController statsToolCtr; + + private boolean reloadRuntime = false; @Autowired private QTI21Service qtiService; @@ -75,19 +92,7 @@ public class QTI21RuntimeController extends RepositoryEntryRuntimeController { RepositoryEntry re, RepositoryEntrySecurity reSecurity, RuntimeControllerCreator runtimeControllerCreator) { super(ureq, wControl, re, reSecurity, runtimeControllerCreator); } - - @Override - protected void initSettingsTools(Dropdown settingsDropdown) { - super.initSettingsTools(settingsDropdown); - if (reSecurity.isEntryAdmin()) { - settingsDropdown.addComponent(new Spacer("")); - qtiOptionsLink = LinkFactory.createToolLink("options", translate("tab.options"), this, "o_sel_repo_options"); - qtiOptionsLink.setIconLeftCSS("o_icon o_icon-fw o_icon_options"); - settingsDropdown.addComponent(qtiOptionsLink); - } - } - @Override protected void initRuntimeTools(Dropdown toolsDropdown) { if (reSecurity.isEntryAdmin()) { @@ -120,6 +125,30 @@ public class QTI21RuntimeController extends RepositoryEntryRuntimeController { toolsDropdown.addComponent(ordersLink); } } + + @Override + protected void initSettingsTools(Dropdown settingsDropdown) { + super.initSettingsTools(settingsDropdown); + if (reSecurity.isEntryAdmin()) { + settingsDropdown.addComponent(new Spacer("")); + + qtiOptionsLink = LinkFactory.createToolLink("options", translate("tab.options"), this, "o_sel_repo_options"); + qtiOptionsLink.setIconLeftCSS("o_icon o_icon-fw o_icon_options"); + settingsDropdown.addComponent(qtiOptionsLink); + } + } + + @Override + protected void initDeleteTools(Dropdown settingsDropdown, boolean needSpacer) { + if (reSecurity.isEntryAdmin()) { + settingsDropdown.addComponent(new Spacer("")); + + resetDataLink = LinkFactory.createToolLink("resetData", translate("tab.reset.data"), this, "o_sel_repo_reset_data"); + resetDataLink.setIconLeftCSS("o_icon o_icon-fw o_icon_delete_item"); + settingsDropdown.addComponent(resetDataLink); + } + super.initDeleteTools(settingsDropdown, !reSecurity.isEntryAdmin()); + } @Override protected void event(UserRequest ureq, Component source, Event event) { @@ -129,26 +158,40 @@ public class QTI21RuntimeController extends RepositoryEntryRuntimeController { doAssessmentTool(ureq); } else if(qtiOptionsLink == source) { doQtiOptions(ureq); + } else if(resetDataLink == source) { + doConfirmResetData(ureq); } else if(toolbarPanel == source) { if(event instanceof PopEvent) { PopEvent pe = (PopEvent)event; Controller popedCtrl = pe.getController(); if(popedCtrl instanceof AssessmentTestComposerController) { AssessmentTestComposerController composerCtrl = (AssessmentTestComposerController)popedCtrl; - if(composerCtrl.hasChanges()) { + if(composerCtrl.hasChanges() || reloadRuntime) { doReloadRuntimeController(ureq); } } else if (popedCtrl instanceof QTI21DeliveryOptionsController) { QTI21DeliveryOptionsController optCtrl = (QTI21DeliveryOptionsController)popedCtrl; - if(optCtrl.hasChanges()) { + if(optCtrl.hasChanges() || reloadRuntime) { doReloadRuntimeController(ureq); } + } else if(reloadRuntime) { + doReloadRuntimeController(ureq); } } } super.event(ureq, source, event); } + @Override + protected void event(UserRequest ureq, Controller source, Event event) { + if(confirmResetDialog == source) { + if(DialogBoxUIFactory.isOkEvent(event) || DialogBoxUIFactory.isYesEvent(event)) { + doReset(ureq); + } + } + super.event(ureq, source, event); + } + private void doReloadRuntimeController(UserRequest ureq) { disposeRuntimeController(); if(reSecurity.isEntryAdmin()) { @@ -158,6 +201,7 @@ public class QTI21RuntimeController extends RepositoryEntryRuntimeController { if(toolbarPanel.getTools().isEmpty()) { initToolbar(); } + reloadRuntime = false; } private Activateable2 doQtiOptions(UserRequest ureq) { @@ -182,10 +226,15 @@ public class QTI21RuntimeController extends RepositoryEntryRuntimeController { WindowControl swControl = addToHistory(ureq, ores, null); if (reSecurity.isEntryAdmin() || reSecurity.isCourseCoach() || reSecurity.isGroupCoach()) { - QTI21AssessmentTestStatisticsController ctrl = new QTI21AssessmentTestStatisticsController(ureq, swControl, getRepositoryEntry(), false); + AssessmentToolOptions asOptions = new AssessmentToolOptions(); + QTI21RuntimeStatisticsController ctrl = new QTI21RuntimeStatisticsController(ureq, swControl, + getRepositoryEntry(), asOptions); listenTo(ctrl); - statsToolCtr = pushController(ureq, "Statistics", ctrl); - currentToolCtr = statsToolCtr; + + + + statsToolCtr = pushController(ureq, translate("command.openteststatistic"), ctrl); + currentToolCtr = ctrl; setActiveTool(testStatisticLink); return statsToolCtr; } @@ -226,4 +275,41 @@ public class QTI21RuntimeController extends RepositoryEntryRuntimeController { boolean hasPassed = assessmentTest.getOutcomeDeclaration(QTI21Constants.PASS_IDENTIFIER) != null; return new AssessableResource(hasScore, hasPassed, true, true, minScore, maxScore, null); } -} + + private void doConfirmResetData(UserRequest ureq) { + String title = translate("reset.test.data.title"); + String text = translate("reset.test.data.text"); + confirmResetDialog = activateOkCancelDialog(ureq, title, text, confirmResetDialog); + } + + private void doReset(UserRequest ureq) { + RepositoryEntry testEntry = getRepositoryEntry(); + List<Identity> identities = repositoryService.getMembers(testEntry); + + //backup + String archiveName = "qti21test_" + + StringHelper.transformDisplayNameToFileSystemName(testEntry.getDisplayname()) + + "_" + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis())) + ".zip"; + Path exportPath = Paths.get(FolderConfig.getCanonicalRoot(), FolderConfig.getUserHomes(), getIdentity().getName(), + "private", "archive", StringHelper.transformDisplayNameToFileSystemName(testEntry.getDisplayname()), archiveName); + File exportFile = exportPath.toFile(); + exportFile.getParentFile().mkdirs(); + + try(FileOutputStream fileStream = new FileOutputStream(exportFile); + ZipOutputStream exportStream = new ZipOutputStream(fileStream)) { + new QTI21ArchiveFormat(getLocale()).export(testEntry, exportStream); + } catch (IOException e) { + logError("", e); + } + + //delete + qtiService.deleteAssessmentTestSession(identities, testEntry, null, null); + + //reload + if(toolbarPanel.size() == 1) { + doReloadRuntimeController(ureq); + } else { + reloadRuntime = true; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeStatisticsController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeStatisticsController.java new file mode 100644 index 0000000000000000000000000000000000000000..fcfbb049a40fcde46ae3a379c852336c40309182 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeStatisticsController.java @@ -0,0 +1,143 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <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 the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <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> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.ui; + +import java.util.Collections; +import java.util.List; + +import org.olat.basesecurity.Group; +import org.olat.core.commons.fullWebApp.LayoutMain3ColsController; +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.components.tree.MenuTree; +import org.olat.core.gui.components.tree.TreeEvent; +import org.olat.core.gui.components.tree.TreeModel; +import org.olat.core.gui.components.tree.TreeNode; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.Event; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.gui.control.controller.BasicController; +import org.olat.core.gui.control.generic.dtabs.Activateable2; +import org.olat.core.id.context.ContextEntry; +import org.olat.core.id.context.StateEntry; +import org.olat.core.util.resource.OresHelper; +import org.olat.course.nodes.ArchiveOptions; +import org.olat.course.nodes.AssessmentToolOptions; +import org.olat.course.nodes.AssessmentToolOptions.AlternativeToIdentities; +import org.olat.ims.qti21.model.QTI21StatisticSearchParams; +import org.olat.ims.qti21.ui.statistics.QTI21StatisticResourceResult; +import org.olat.repository.RepositoryEntry; + +/** + * + * Initial date: 05.08.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QTI21RuntimeStatisticsController extends BasicController implements Activateable2 { + + private MenuTree courseTree; + private Controller currentCtrl; + private LayoutMain3ColsController layoutCtr; + + private final ArchiveOptions options; + private final QTI21StatisticSearchParams searchParams; + private final QTI21StatisticResourceResult resourceResult; + + public QTI21RuntimeStatisticsController(UserRequest ureq, WindowControl wControl, + RepositoryEntry testEntry, AssessmentToolOptions asOptions) { + super(ureq, wControl); + this.options = new ArchiveOptions(); + this.options.setGroup(asOptions.getGroup()); + this.options.setIdentities(asOptions.getIdentities()); + + searchParams = new QTI21StatisticSearchParams(testEntry, null, null); + if(asOptions.getGroup() != null) { + List<Group> bGroups = Collections.singletonList(asOptions.getGroup().getBaseGroup()); + searchParams.setLimitToGroups(bGroups); + } else if(asOptions.getAlternativeToIdentities() != null) { + AlternativeToIdentities alt = asOptions.getAlternativeToIdentities(); + searchParams.setMayViewAllUsersAssessments(alt.isMayViewAllUsersAssessments()); + searchParams.setLimitToGroups(alt.getGroups()); + } + + resourceResult = new QTI21StatisticResourceResult(testEntry, searchParams); + + TreeModel treeModel = resourceResult.getTreeModel(); + + courseTree = new MenuTree("qti21StatisticsTree"); + courseTree.setTreeModel(treeModel); + courseTree.addListener(this); + + layoutCtr = new LayoutMain3ColsController(ureq, wControl, courseTree, new Panel("empty"), null); + putInitialPanel(layoutCtr.getInitialComponent()); + + TreeNode rootNode = courseTree.getTreeModel().getRootNode(); + doSelectNode(ureq, rootNode); + } + + @Override + protected void doDispose() { + // + } + + @Override + public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) { + if(entries == null || entries.isEmpty()) return; + + ContextEntry entry = entries.get(0); + if(entry.getOLATResourceable() != null && entry.getOLATResourceable().getResourceableTypeName() != null) { + String nodeId = entry.getOLATResourceable().getResourceableTypeName(); + TreeNode nclr = courseTree.getTreeModel().getNodeById(nodeId); + if(nclr != null) { + String selNodeId = nclr.getIdent(); + courseTree.setSelectedNodeId(selNodeId); + doSelectNode(ureq, nclr); + } + } + } + + @Override + protected void event(UserRequest ureq, Component source, Event event) { + if(courseTree == source) { + if(event instanceof TreeEvent) { + TreeEvent te = (TreeEvent)event; + if(MenuTree.COMMAND_TREENODE_CLICKED.equals(te.getCommand())) { + String ident = te.getNodeId(); + TreeNode selectedNode = courseTree.getTreeModel().getNodeById(ident); + doSelectNode(ureq, selectedNode); + } + } + } + } + + private void doSelectNode(UserRequest ureq, TreeNode selectedNode) { + removeAsListenerAndDispose(currentCtrl); + WindowControl swControl = addToHistory(ureq, OresHelper.createOLATResourceableInstance(selectedNode.getIdent(), 0l), null); + currentCtrl = resourceResult.getController(ureq, swControl, selectedNode, false); + if(currentCtrl != null) { + listenTo(currentCtrl); + layoutCtr.setCol3(currentCtrl.getInitialComponent()); + } else { + layoutCtr.setCol3(new Panel("empty")); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties index 4e88d9d4073d93dc8fc15df97852c99d69cd91a8..d28e5f6308c53c7fc2ade2dd71967717059760fe 100644 --- a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties @@ -47,6 +47,8 @@ debug.outcomes=Output Daten debug.responses=Antworten Daten exploded.msg=Explodiert form.metadata.title=Title +reset.test.data.title=Daten von Test zurücksetzen +reset.test.data.text=Wollen Sie wirklich alle Daten von dem Test zurücksetzen? Die Resultate werden definitiv gelöscht. head.assessment.details=$org.olat.ims.qti\:head.ass.details head.assessment.overview=$org.olat.ims.qti\:head.ass.details interaction.order.drag.msg=Ziehen Sie die nicht verwendete Elemente von hier... @@ -89,7 +91,7 @@ suspend.test=$org.olat.modules.iq\:suspendAssess tab.options=Optionen table.header.lastModified=$org.olat.course.nodes.iq\:table.header.lastModified table.header.results=$org.olat.course.nodes.iq\:table.header.results - +tab.reset.data=Daten zurücksetzen terminated.msg=Der Test ist beendet. test.complete=Test abgeschlossen test.entry.page.text=Der Test hat bis {0} Teile. diff --git a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties index 782bc1ccdc9b7fce20949b326e9336acddddef5d..38e302abef3aa642a1fa6254d6fdd58cb3680106 100644 --- a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties @@ -28,9 +28,9 @@ assessment.test.enter.test=Enter Test assessment.test.modal.feedback=Feedback assessment.test.multiPartTestMenu=Test part question menu assessment.test.nav.title.multiPartTestMenu=Test part question menu -assessment.test.nav.title.questionMenu=Test Question Menu -assessment.test.nextQuestion=Next Question -assessment.test.questionMenu=Test Question Menu +assessment.test.nav.title.questionMenu=Test question menu +assessment.test.nextQuestion=Next question +assessment.test.questionMenu=Test question menu assessment.test.suspended=The test has been suspended. assessment.testpart.config=Test part attemptsleft=$org.olat.modules.iq\:attemptsleft @@ -50,6 +50,7 @@ error.input.invalid=Your input must be a valid {0} error.input.invalid.record=number error.input.invalid.string=text exploded.msg=Exploded +menu.reset.title=Reset data of test form.metadata.title=Title head.assessment.details=$org.olat.ims.qti\:head.ass.details head.assessment.overview=$org.olat.ims.qti\:head.ass.details @@ -96,6 +97,7 @@ serialize.error=An unexpected happens while saving the file. submit=Submit response suspend.test=$org.olat.modules.iq\:suspendAssess tab.options=Options +tab.reset.data=Reset data table.header.lastModified=$org.olat.course.nodes.iq\:table.header.lastModified table.header.results=$org.olat.course.nodes.iq\:table.header.results terminated.msg=The test is finished. diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java index 61b2579d13c4c1953aea56d584bf61984dcceeb3..e46c64074d165474af1f2c3ae578777cc81f1bac 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java @@ -814,7 +814,13 @@ public abstract class AssessmentObjectComponentRenderer extends DefaultComponent if(StringHelper.containsNonWhitespace(checkJavascript)) { sb.append(" onchange=\"").append(checkJavascript).append("\">"); } - if(StringHelper.containsNonWhitespace(responseInputString)) { + + if(renderer.isSolutionMode()) { + String placeholder = interaction.getPlaceholderText(); + if(StringHelper.containsNonWhitespace(placeholder)) { + sb.append(placeholder); + } + } else if( StringHelper.containsNonWhitespace(responseInputString)) { sb.append(responseInputString); } sb.append("</textarea>"); diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java index 4dc2bbfc3b1c28fbe2a66f9a0066ceea703803f8..a877f65b318ba8d7c872e1c0439a2201e72ee99a 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java @@ -229,7 +229,9 @@ public class AssessmentRenderFunctions { } public static ResponseData getResponseInput(ItemSessionState itemSessionState, Identifier identifier) { - return itemSessionState.getRawResponseDataMap().get(identifier); + ResponseData responseInput = itemSessionState.getRawResponseDataMap().get(identifier); + + return responseInput; } public static String extractSingleCardinalityResponseInput(ResponseData data) { diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java index 28d4bdee476a04e59e463f8d5850ec52aa034144..231f01864a6dbbb76ff7335e0b6bf3e422d5f101 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java @@ -551,16 +551,16 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe .append(itemNode.getSectionPartTitle()).append("</span>"); if(!reviewable) { - sb.append("<span class='itemStatus reviewNotAllowed'>").append(translator.translate("assessment.item.status.reviewNot")).append("</span>"); + sb.append("<span class='o_assessmentitem_status reviewNotAllowed'>").append(translator.translate("assessment.item.status.reviewNot")).append("</span>"); } else if(itemSessionState.getUnboundResponseIdentifiers().size() > 0 || itemSessionState.getInvalidResponseIdentifiers().size() > 0) { - sb.append("<span class='itemStatus reviewInvalid'>").append(translator.translate("assessment.item.status.reviewInvalidAnswer")).append("</span>"); + sb.append("<span class='o_assessmentitem_status reviewInvalid'>").append(translator.translate("assessment.item.status.reviewInvalidAnswer")).append("</span>"); } else if(itemSessionState.isResponded()) { - sb.append("<span class='itemStatus review'>").append(translator.translate("assessment.item.status.review")).append("</span>"); + sb.append("<span class='o_assessmentitem_status review'>").append(translator.translate("assessment.item.status.review")).append("</span>"); } else if(itemSessionState.getEntryTime() != null) { - sb.append("<span class='itemStatus reviewNotAnswered'>").append(translator.translate("assessment.item.status.reviewNotAnswered")).append("</span>"); + sb.append("<span class='o_assessmentitem_status reviewNotAnswered'>").append(translator.translate("assessment.item.status.reviewNotAnswered")).append("</span>"); } else { - sb.append("<span class='itemStatus reviewNotSeen'>").append(translator.translate("assessment.item.status.reviewNotSeen")).append("</span>"); + sb.append("<span class='o_assessmentitem_status reviewNotSeen'>").append(translator.translate("assessment.item.status.reviewNotSeen")).append("</span>"); } sb.append("</button></li>"); @@ -606,7 +606,7 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe boolean multiPartTest = component.hasMultipleTestParts(); String title = multiPartTest ? translator.translate("assessment.test.nav.title.multiPartTestMenu") : translator.translate("assessment.test.nav.title.questionMenu"); - sb.append("<h1>").append(title).append(" Question Menu</h1>"); + sb.append("<h3>").append(title).append("</h3>"); //part, sections and item refs sb.append("<ul class='o_testpartnavigation list-unstyled'>"); @@ -651,7 +651,7 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe private void renderNavigationAssessmentSection(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode sectionNode, URLBuilder ubu, Translator translator) { sb.append("<li class='o_assessmentsection o_qti_menu_item'>") - .append("<header><h2>").append(sectionNode.getSectionPartTitle()).append("</h2>"); + .append("<header><h4>").append(sectionNode.getSectionPartTitle()).append("</h4>"); renderAssessmentSectionRubrickBlock(renderer, sb, component, sectionNode, ubu, translator); sb.append("</header><ul class='o_testpartnavigation_inner list-unstyled'>"); @@ -686,16 +686,16 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe ItemSessionState itemSessionState = component.getItemSessionState(itemNode.getKey()); if(itemSessionState.getEndTime() != null) { - sb.append("<span class='itemStatus ended'>Finished</span>"); + sb.append("<span class='o_assessmentitem_status ended'>Finished</span>"); } else if(itemSessionState.getUnboundResponseIdentifiers().size() > 0 || itemSessionState.getInvalidResponseIdentifiers().size() > 0) { - sb.append("<span class='itemStatus invalid'>Needs Attention</span>"); + sb.append("<span class='o_assessmentitem_status invalid'>Needs Attention</span>"); } else if(itemSessionState.isResponded() || itemSessionState.hasUncommittedResponseValues()) { - sb.append("<span class='itemStatus answered'>Answered</span>"); + sb.append("<span class='o_assessmentitem_status answered'>Answered</span>"); } else if(itemSessionState.getEntryTime() != null) { - sb.append("<span class='itemStatus notAnswered'>Not Answered</span>"); + sb.append("<span class='o_assessmentitem_status notAnswered'>Not Answered</span>"); } else { - sb.append("<span class='itemStatus notPresented'>").append(translator.translate("assessment.item.status.notSeen")).append("</span>"); + sb.append("<span class='o_assessmentitem_status notPresented'>").append(translator.translate("assessment.item.status.notSeen")).append("</span>"); } sb.append("</button>"); diff --git a/src/main/java/org/olat/ims/qti21/ui/components/InteractionResultComponentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/components/InteractionResultComponentRenderer.java index 8ffa2952364157e1ce132f4dbcbf461b1e8f394b..61782b5f8e622779ccf04f3ffab53a86b3627a9c 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/InteractionResultComponentRenderer.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/InteractionResultComponentRenderer.java @@ -27,6 +27,7 @@ import org.olat.core.gui.render.URLBuilder; import org.olat.core.gui.translator.Translator; import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; +import uk.ac.ed.ph.jqtiplus.node.content.basic.Flow; import uk.ac.ed.ph.jqtiplus.node.content.variable.PrintedVariable; import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; @@ -60,6 +61,8 @@ public class InteractionResultComponentRenderer extends AssessmentObjectComponen if(interaction instanceof Block) { renderBlock(assessmentRenderer, sb, cmp, resolvedAssessmentItem, itemSessionState, (Block)interaction, ubu, translator); + } else if(interaction instanceof Flow) { + renderFlow(assessmentRenderer, sb, cmp, resolvedAssessmentItem, itemSessionState, (Flow)interaction, ubu, translator); } } diff --git a/src/main/java/org/olat/ims/qti21/ui/components/_content/extendedTextInteraction.html b/src/main/java/org/olat/ims/qti21/ui/components/_content/extendedTextInteraction.html index 681e1ea2fd70134b795a4506de61478673371fac..68b6e4bb5b34796734ebae3d99a829641cabcc8e 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/_content/extendedTextInteraction.html +++ b/src/main/java/org/olat/ims/qti21/ui/components/_content/extendedTextInteraction.html @@ -14,7 +14,6 @@ $r.translate("error.input.invalid", $r.translate("error.input.invalid.${responseDeclaration.cardinality.toQtiString()}")) </div> #end - #if($r.isInvalidResponse($interaction.responseIdentifier)) <div class="o_error badResponse"> ## This will happen if either a pattern is wrong or the wrong number of choices were made diff --git a/src/main/java/org/olat/ims/qti21/ui/components/_content/textEntryInteraction.html b/src/main/java/org/olat/ims/qti21/ui/components/_content/textEntryInteraction.html index d98deb143c39b0fc10a9aca7d216f975784504a1..4039f65d9eba10feae0f587b99d45c9cae70b882 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/_content/textEntryInteraction.html +++ b/src/main/java/org/olat/ims/qti21/ui/components/_content/textEntryInteraction.html @@ -2,12 +2,13 @@ #set($isBadResponse = $r.isBadResponse($interaction.responseIdentifier)) #set($isInvalidResponse = $r.isInvalidResponse($interaction.responseIdentifier)) #set($responseDeclaration = $r.getResponseDeclaration($interaction.responseIdentifier)) -#set($responseInput = $r.getResponseInput($interaction.responseIdentifier)) -#set($responseInputString = $r.extractSingleCardinalityResponseInput($responseInput)) +#set($responseInput = $r.getResponseValue($interaction.responseIdentifier)) +#set($responseInputString = $r.toString($responseInput)) + #set($checkJavaScript = $r.checkJavaScript($responseDeclaration,$interaction.patternmask)) <input name="qtiworks_presented_${responseIdentifier}" type="hidden" value="1"/> <span class="$localName"> - <input id="od_${responseIdentifier}" type="text" name="qtiworks_response_${responseIdentifier}" #if($responseInputString && !$responseInputString.isEmpty()) value="$responseInputString" #else value="" #end $r.placeholder($interaction) #if($r.isItemSessionEnded()) disabled #end #if($isBadResponse) class='badResponse' #end #if($interaction.expectedLength) size='$interaction.expectedLength' #end #if($checks && $checks.size() > 0) onchange='$checkJavaScript' #end autocomplete="off"/> + <input id="od_${responseIdentifier}" type="text" name="qtiworks_response_${responseIdentifier}" #if($responseInputString && !$responseInputString.isEmpty()) value="$responseInputString" #else value="" #end #if(!$r.isItemSessionEnded()) $r.placeholder($interaction) #end #if($r.isItemSessionEnded()) disabled #end #if($isBadResponse) class='badResponse' #end #if($interaction.expectedLength) size='$interaction.expectedLength' #end #if($checks && $checks.size() > 0) onchange='$checkJavaScript' #end autocomplete="off"/> #if($isBadResponse) <span class="badResponse"> You must enter a valid diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentItemStatisticsController.java b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentItemStatisticsController.java index 39c7f4db3870983d5f35907b9015b98b43c30385..2d8daa1eb4d12bd0ba6b4d9d9696e48381a96582 100644 --- a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentItemStatisticsController.java +++ b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentItemStatisticsController.java @@ -37,6 +37,7 @@ import org.olat.core.util.StringHelper; import org.olat.ims.qti.statistics.QTIType; import org.olat.ims.qti.statistics.model.StatisticsItem; import org.olat.ims.qti21.QTI21StatisticsManager; +import org.olat.ims.qti21.model.QTI21QuestionType; import org.olat.ims.qti21.model.QTI21StatisticSearchParams; import org.olat.ims.qti21.ui.statistics.interactions.ChoiceInteractionStatisticsController; import org.olat.ims.qti21.ui.statistics.interactions.HotspotInteractionStatisticsController; @@ -89,9 +90,16 @@ public class QTI21AssessmentItemStatisticsController extends BasicController { if(StringHelper.containsNonWhitespace(sectionTitle)) { mainVC.contextPut("sectionTitle", sectionTitle); } - mainVC.contextPut("numOfParticipants", resourceResult.getQTIStatisticAssessment().getNumOfParticipants()); + mainVC.contextPut("numOfParticipants", numOfParticipants); mainVC.contextPut("printMode", new Boolean(printMode)); + QTI21QuestionType type = QTI21QuestionType.getType(item); + if(type != null) { + mainVC.contextPut("itemCss", type.getCssClass()); + } else { + mainVC.contextPut("itemCss", "o_mi_qtiunkown"); + } + StatisticsItem itemStats = initItemStatistics(); List<String> interactionIds = initInteractionControllers(ureq, itemStats); mainVC.contextPut("interactionIds", interactionIds); diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticResourceResult.java b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticResourceResult.java index 3615ce18ecc2bfc7056be80ab098c92e335941d6..65823f067631d52ce39c8312239ed687e0ae01ee 100644 --- a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticResourceResult.java +++ b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticResourceResult.java @@ -43,6 +43,7 @@ import org.olat.ims.qti.statistics.QTIType; import org.olat.ims.qti.statistics.model.StatisticAssessment; import org.olat.ims.qti21.QTI21Service; import org.olat.ims.qti21.QTI21StatisticsManager; +import org.olat.ims.qti21.model.QTI21QuestionType; import org.olat.ims.qti21.model.QTI21StatisticSearchParams; import org.olat.repository.RepositoryEntry; @@ -76,6 +77,10 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult { private final QTI21Service qtiService; private final QTI21StatisticsManager qtiStatisticsManager; + public QTI21StatisticResourceResult(RepositoryEntry testEntry, QTI21StatisticSearchParams searchParams) { + this(testEntry, null, null, searchParams); + } + public QTI21StatisticResourceResult(RepositoryEntry testEntry, RepositoryEntry courseEntry, QTICourseNode courseNode, QTI21StatisticSearchParams searchParams) { @@ -119,7 +124,39 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult { URI itemUri = resolvedAssessmentTest.getSystemIdByItemRefMap().get(itemRef); return new File(itemUri); } + + /** + * Return the tree model for a test learn resource. + * + * @return + */ + public TreeModel getTreeModel() { + GenericTreeModel treeModel = new GenericTreeModel(); + GenericTreeNode rootTreeNode = new GenericTreeNode(); + treeModel.setRootNode(rootTreeNode); + + FileResourceManager frm = FileResourceManager.getInstance(); + File unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource()); + resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, false); + AssessmentTest test = resolvedAssessmentTest.getTestLookup().getRootNodeHolder().getRootNode(); + + rootTreeNode.setTitle(test.getTitle()); + rootTreeNode.setUserObject(test); + rootTreeNode.setIconCssClass("o_icon o_icon-lg o_qtiassessment_icon"); + + //list all test parts + List<TestPart> parts = test.getChildAbstractParts(); + int counter = 0; + for(TestPart part:parts) { + buildRecursively(part, ++counter, rootTreeNode); + } + return treeModel; + } + /** + * Return the tree model for a course and a specific test. + * + */ @Override public TreeModel getSubTreeModel() { GenericTreeModel subTreeModel = new GenericTreeModel(); @@ -191,7 +228,13 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult { if(ex == null) { AssessmentItem assessmentItem = resolvedAssessmentItem.getItemLookup().getRootNodeHolder().getRootNode(); itemNode.setTitle(assessmentItem.getTitle()); - itemNode.setIconCssClass("o_icon o_mi_qtisc"); + + QTI21QuestionType type = QTI21QuestionType.getType(assessmentItem); + if(type != null) { + itemNode.setIconCssClass("o_icon ".concat(type.getCssClass())); + } else { + itemNode.setIconCssClass("o_icon o_mi_qtiunkown"); + } itemNode.setUserObject(itemRef); parentNode.addChild(itemNode); } @@ -201,40 +244,52 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult { @Override public Controller getController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel, TreeNode selectedNode) { - return getController(ureq, wControl, stackPanel, selectedNode, false); + return getController(ureq, wControl, selectedNode, false); } - public Controller getController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel, + public Controller getController(UserRequest ureq, WindowControl wControl, TreeNode selectedNode, boolean printMode) { if(selectedNode instanceof StatisticResourceNode) { - return createAssessmentController(ureq, wControl, stackPanel, printMode); + return createAssessmentController(ureq, wControl, printMode); } else { Object uobject = selectedNode.getUserObject(); + if(uobject instanceof AssessmentItemRef) { TreeNode parentNode = (TreeNode)selectedNode.getParent(); String sectionTitle = parentNode.getTitle(); - return createAssessmentItemController(ureq, wControl, stackPanel, + return createAssessmentItemController(ureq, wControl, (AssessmentItemRef)uobject, sectionTitle, printMode); + } else if(uobject instanceof AssessmentTest) { + return createAssessmentController(ureq, wControl, printMode); } } return null; } - private Controller createAssessmentController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel, + private Controller createAssessmentController(UserRequest ureq, WindowControl wControl, boolean printMode) { - Controller ctrl = new QTI21AssessmentTestStatisticsController(ureq, wControl, this, printMode); - CourseNodeConfiguration cnConfig = CourseNodeFactory.getInstance() - .getCourseNodeConfigurationEvenForDisabledBB(courseNode.getType()); - String iconCssClass = cnConfig.getIconCSSClass(); - return TitledWrapperHelper.getWrapper(ureq, wControl, ctrl, courseNode, iconCssClass); + Controller ctrl; + if(courseNode == null) { + ctrl = new QTI21AssessmentTestStatisticsController(ureq, wControl, testEntry, printMode); + } else { + ctrl = new QTI21AssessmentTestStatisticsController(ureq, wControl, this, printMode); + CourseNodeConfiguration cnConfig = CourseNodeFactory.getInstance() + .getCourseNodeConfigurationEvenForDisabledBB(courseNode.getType()); + String iconCssClass = cnConfig.getIconCSSClass(); + ctrl = TitledWrapperHelper.getWrapper(ureq, wControl, ctrl, courseNode, iconCssClass); + } + return ctrl; } - private Controller createAssessmentItemController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel, + private Controller createAssessmentItemController(UserRequest ureq, WindowControl wControl, AssessmentItemRef assessmentItemRef, String sectionTitle, boolean printMode) { ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(assessmentItemRef); AssessmentItem assessmentItem = resolvedAssessmentItem.getItemLookup().getRootNodeHolder().getRootNode(); Controller ctrl = new QTI21AssessmentItemStatisticsController(ureq, wControl, assessmentItemRef, assessmentItem, sectionTitle, this, printMode); String iconCssClass = "o_mi_qtisc"; - return TitledWrapperHelper.getWrapper(ureq, wControl, ctrl, courseNode, iconCssClass); + if(courseNode != null) { + ctrl = TitledWrapperHelper.getWrapper(ureq, wControl, ctrl, courseNode, iconCssClass); + } + return ctrl; } } diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/_content/statistics_item.html b/src/main/java/org/olat/ims/qti21/ui/statistics/_content/statistics_item.html index 1494ef953a6875e2b3ac48dab4238726e545329d..6f21adb2cb3fbdf0eb8ed4308a060092113025fe 100644 --- a/src/main/java/org/olat/ims/qti21/ui/statistics/_content/statistics_item.html +++ b/src/main/java/org/olat/ims/qti21/ui/statistics/_content/statistics_item.html @@ -2,7 +2,7 @@ #if($sectionTitle) <h5>$r.translate("section"): $sectionTitle</h5> #end - <h3><i class="o_icon $series.itemCss""> </i> $title</h3> + <h3><i class="o_icon $itemCss""> </i> $title</h3> #if ($question && $question != "") <h4>$r.translate("chart.item")</h4> <div class="o_qti_statistics_question clearfix">$question</div> diff --git a/src/main/java/org/olat/modules/assessment/manager/AssessmentEntryDAO.java b/src/main/java/org/olat/modules/assessment/manager/AssessmentEntryDAO.java index 3e80f5039a842f6e24d446e80b14622c5a56c829..37bda08e0f92080a1ee47ed3686dd6c76d63dc3e 100644 --- a/src/main/java/org/olat/modules/assessment/manager/AssessmentEntryDAO.java +++ b/src/main/java/org/olat/modules/assessment/manager/AssessmentEntryDAO.java @@ -31,6 +31,7 @@ import org.olat.core.commons.persistence.DB; import org.olat.core.id.Identity; import org.olat.modules.assessment.AssessmentEntry; import org.olat.modules.assessment.model.AssessmentEntryImpl; +import org.olat.modules.assessment.model.AssessmentEntryStatus; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryEntryRef; import org.springframework.beans.factory.annotation.Autowired; @@ -176,6 +177,16 @@ public class AssessmentEntryDAO { return entries.isEmpty() ? null : entries.get(0); } + public AssessmentEntry resetAssessmentEntry(AssessmentEntry nodeAssessment) { + nodeAssessment.setScore(null); + nodeAssessment.setPassed(null); + nodeAssessment.setAttempts(0); + nodeAssessment.setCompletion(null); + nodeAssessment.setAssessmentStatus(AssessmentEntryStatus.notStarted); + ((AssessmentEntryImpl)nodeAssessment).setLastModified(new Date()); + return dbInstance.getCurrentEntityManager().merge(nodeAssessment); + } + public AssessmentEntry updateAssessmentEntry(AssessmentEntry nodeAssessment) { ((AssessmentEntryImpl)nodeAssessment).setLastModified(new Date()); return dbInstance.getCurrentEntityManager().merge(nodeAssessment); diff --git a/src/main/java/org/olat/repository/ui/RepositoryEntryRuntimeController.java b/src/main/java/org/olat/repository/ui/RepositoryEntryRuntimeController.java index 20f86dded2ea00771f8c5ba7068f92266f0ec6f1..3573c666794b1f91878445582ddc00c7abc7963f 100644 --- a/src/main/java/org/olat/repository/ui/RepositoryEntryRuntimeController.java +++ b/src/main/java/org/olat/repository/ui/RepositoryEntryRuntimeController.java @@ -141,7 +141,7 @@ public class RepositoryEntryRuntimeController extends MainLayoutBasicController private LockResult lockResult; private boolean assessmentLock;// by Assessment mode private AssessmentMode assessmentMode; - private final RepositoryHandler handler; + protected final RepositoryHandler handler; private AtomicBoolean launchDateUpdated = new AtomicBoolean(false); private HistoryPoint launchedFromPoint; @@ -155,7 +155,7 @@ public class RepositoryEntryRuntimeController extends MainLayoutBasicController @Autowired protected RepositoryModule repositoryModule; @Autowired - private RepositoryService repositoryService; + protected RepositoryService repositoryService; @Autowired protected RepositoryManager repositoryManager; @Autowired @@ -299,6 +299,7 @@ public class RepositoryEntryRuntimeController extends MainLayoutBasicController initRuntimeTools(toolsDropdown); initSettingsTools(settingsDropdown); initEditionTools(settingsDropdown); + initDeleteTools(settingsDropdown, true); detailsLink = LinkFactory.createToolLink("details", translate("details.header"), this, "o_sel_repo_details"); detailsLink.setIconLeftCSS("o_icon o_icon-fw o_icon_details"); @@ -385,34 +386,21 @@ public class RepositoryEntryRuntimeController extends MainLayoutBasicController settingsDropdown.addComponent(downloadLink); } } - - boolean canClose = OresHelper.isOfType(re.getOlatResource(), CourseModule.class); - boolean closeManged = RepositoryEntryManagedFlag.isManaged(re, RepositoryEntryManagedFlag.close); - + } + + protected void initDeleteTools(Dropdown settingsDropdown, boolean needSpacer) { if(reSecurity.isEntryAdmin()) { boolean deleteManaged = RepositoryEntryManagedFlag.isManaged(re, RepositoryEntryManagedFlag.delete); - if(settingsDropdown.size() > 0 && (canClose || !deleteManaged)) { + if(needSpacer && settingsDropdown.size() > 0 && !deleteManaged) { settingsDropdown.addComponent(new Spacer("close-delete")); } - - if(canClose && (!closeManged || !deleteManaged)) { - // If a resource is closable (currently only course) and - // deletable (currently all resources) we offer those two - // actions in a separate page, unless both are managed - // operations. In that case we don't show anything at all. - // If only one of the two actions are managed, we go to the - // separate page as well and show only the relevant action - // there. - lifeCycleChangeLink = LinkFactory.createToolLink("lifeCycleChange", translate("details.lifecycle.change"), this, "o_icon o_icon-fw o_icon_lifecycle"); - settingsDropdown.addComponent(lifeCycleChangeLink); - } else { - if(!deleteManaged) { - String type = translate(handler.getSupportedType()); - String deleteTitle = translate("details.delete.alt", new String[]{ type }); - deleteLink = LinkFactory.createToolLink("delete", deleteTitle, this, "o_icon o_icon-fw o_icon_delete_item"); - deleteLink.setElementCssClass("o_sel_repo_close"); - settingsDropdown.addComponent(deleteLink); - } + + if(!deleteManaged) { + String type = translate(handler.getSupportedType()); + String deleteTitle = translate("details.delete.alt", new String[]{ type }); + deleteLink = LinkFactory.createToolLink("delete", deleteTitle, this, "o_icon o_icon-fw o_icon_delete_item"); + deleteLink.setElementCssClass("o_sel_repo_close"); + settingsDropdown.addComponent(deleteLink); } } } @@ -904,7 +892,7 @@ public class RepositoryEntryRuntimeController extends MainLayoutBasicController protected void launchContent(UserRequest ureq, RepositoryEntrySecurity security) { if(corrupted) { - runtimeController = new CorruptedCourseController(ureq, this.getWindowControl()); + runtimeController = new CorruptedCourseController(ureq, getWindowControl()); listenTo(runtimeController); toolbarPanel.rootController(re.getDisplayname(), runtimeController); } else if(security.canLaunch()) {