diff --git a/src/main/java/org/olat/core/commons/services/taskexecutor/manager/PersistentTaskDAO.java b/src/main/java/org/olat/core/commons/services/taskexecutor/manager/PersistentTaskDAO.java index fb1d2f52f307924698aa95a7d5baa7b160f59024..3a111dc18fbda0af780ff0c27b7d15e910743d48 100644 --- a/src/main/java/org/olat/core/commons/services/taskexecutor/manager/PersistentTaskDAO.java +++ b/src/main/java/org/olat/core/commons/services/taskexecutor/manager/PersistentTaskDAO.java @@ -110,14 +110,16 @@ public class PersistentTaskDAO { } public PersistentTask loadTaskById(Long taskKey) { - PersistentTask task = dbInstance.getCurrentEntityManager() - .find(PersistentTask.class, taskKey); - return task; + return dbInstance.getCurrentEntityManager().find(PersistentTask.class, taskKey); } - public PersistentTask pickTaskForRun(Long taskKey) { - PersistentTask task = dbInstance.getCurrentEntityManager() - .find(PersistentTask.class, taskKey, LockModeType.PESSIMISTIC_WRITE); + public synchronized PersistentTask pickTaskForRun(PersistentTask task) { + if(task != null) {// remove it from the cache + dbInstance.getCurrentEntityManager().detach(task); + } + + task = dbInstance.getCurrentEntityManager() + .find(PersistentTask.class, task.getKey(), LockModeType.PESSIMISTIC_WRITE); if(task != null) { if(TaskStatus.newTask.equals(task.getStatus())) { task.setStatus(TaskStatus.inWork); @@ -125,9 +127,15 @@ public class PersistentTaskDAO { task.setExecutorBootId(WebappHelper.getBootId()); task = dbInstance.getCurrentEntityManager().merge(task); } else if(TaskStatus.inWork.equals(task.getStatus())) { - task.setExecutorNode(Integer.toString(WebappHelper.getNodeId())); - task.setExecutorBootId(WebappHelper.getBootId()); - task = dbInstance.getCurrentEntityManager().merge(task); + if(WebappHelper.getBootId().equals(task.getExecutorBootId())) { + // someone has already pick it + task = null; + } else { + // reboot of a task in work + task.setExecutorNode(Integer.toString(WebappHelper.getNodeId())); + task.setExecutorBootId(WebappHelper.getBootId()); + task = dbInstance.getCurrentEntityManager().merge(task); + } } else if(TaskStatus.edition.equals(task.getStatus())) { task = null; } diff --git a/src/main/java/org/olat/core/commons/services/taskexecutor/model/PersistentTaskRunnable.java b/src/main/java/org/olat/core/commons/services/taskexecutor/model/PersistentTaskRunnable.java index e773c109e6d0cb99139374a01b4e0eedf0cc472e..3340081af96c1b25ec121416befc77e6971f06d0 100644 --- a/src/main/java/org/olat/core/commons/services/taskexecutor/model/PersistentTaskRunnable.java +++ b/src/main/java/org/olat/core/commons/services/taskexecutor/model/PersistentTaskRunnable.java @@ -41,17 +41,20 @@ public class PersistentTaskRunnable implements Runnable { @Override public void run() { - PersistentTask task = null; PersistentTaskDAO taskDao = CoreSpringFactory.getImpl(PersistentTaskDAO.class); + PersistentTask task = null; try { - task = taskDao.pickTaskForRun(taskKey); + task = taskDao.loadTaskById(taskKey); if(task != null) { - Runnable runnable = taskDao.deserializeTask(task); - if(runnable instanceof TaskAwareRunnable) { - ((TaskAwareRunnable)runnable).setTask(task); + task = taskDao.pickTaskForRun(task); + if(task != null) { + Runnable runnable = taskDao.deserializeTask(task); + if(runnable instanceof TaskAwareRunnable) { + ((TaskAwareRunnable)runnable).setTask(task); + } + runnable.run(); + taskDao.taskDone(task); } - runnable.run(); - taskDao.taskDone(task); } DBFactory.getInstance().commitAndCloseSession(); } catch (Throwable e) { diff --git a/src/main/java/org/olat/core/util/pdf/PdfDocument.java b/src/main/java/org/olat/core/util/pdf/PdfDocument.java index 221d8dc3a32513d55269358ad9f6ac1a7510f66a..5f38d9177d6fca6e4ae2a9ea5e55aee4a3579ca3 100644 --- a/src/main/java/org/olat/core/util/pdf/PdfDocument.java +++ b/src/main/java/org/olat/core/util/pdf/PdfDocument.java @@ -149,6 +149,12 @@ public class PdfDocument { while (text.length() > 0) { int spaceIndex = text.indexOf(' ', lastSpace + 1); if (spaceIndex < 0) { + float size = getStringWidth(text, textFont, fontSize); + if (size > paragraphWidth) { + String subString = text.substring(0, lastSpace); + lines.add(subString.trim()); + text = text.substring(lastSpace).trim(); + } lines.add(text); text = ""; } else { diff --git a/src/main/java/org/olat/course/nodes/members/MembersCourseNodeRunController.java b/src/main/java/org/olat/course/nodes/members/MembersCourseNodeRunController.java index e4e557d4b7e02266004ddb3e29db030d6f505310..1af2eb150e8a14722c7b55f1a57f9830b0a951cf 100644 --- a/src/main/java/org/olat/course/nodes/members/MembersCourseNodeRunController.java +++ b/src/main/java/org/olat/course/nodes/members/MembersCourseNodeRunController.java @@ -79,7 +79,7 @@ public class MembersCourseNodeRunController extends BasicController { RepositoryEntry courseRepositoryEntry = courseEnv.getCourseGroupManager().getCourseEntry(); List<Identity> owners; - List<Identity> coaches = new ArrayList<>(); + List<Identity> coaches; List<Identity> participants = new ArrayList<>(); boolean showOwners = config.getBooleanSafe(MembersCourseNode.CONFIG_KEY_SHOWOWNER); @@ -105,8 +105,10 @@ public class MembersCourseNodeRunController extends BasicController { MembersCourseNode.CONFIG_KEY_COACHES_CUR_ELEMENT)) { CourseGroupManager cgm = courseEnv.getCourseGroupManager(); - MembersHelpers.addCoaches(config, cgm, businessGroupService, coaches); + coaches = MembersHelpers.getCoaches(config, cgm, businessGroupService); showCoaches = true; + } else { + coaches = Collections.emptyList(); } boolean showParticipants = false; @@ -115,8 +117,10 @@ public class MembersCourseNodeRunController extends BasicController { MembersCourseNode.CONFIG_KEY_PARTICIPANTS_CUR_ELEMENT)) { CourseGroupManager cgm = courseEnv.getCourseGroupManager(); - MembersHelpers.addParticipants(config, cgm, businessGroupService, participants); + participants = MembersHelpers.getParticipants(config, cgm, businessGroupService); showParticipants = true; + } else { + participants = Collections.emptyList(); } Map<Long,CurriculumMemberInfos> curriculumInfos = null; diff --git a/src/main/java/org/olat/course/nodes/members/MembersHelpers.java b/src/main/java/org/olat/course/nodes/members/MembersHelpers.java index cad47689d7d41d3a20f3d062204f283eda01c77d..a1fc4cb1d99c022f25c59c4dc36b35502eb16781 100644 --- a/src/main/java/org/olat/course/nodes/members/MembersHelpers.java +++ b/src/main/java/org/olat/course/nodes/members/MembersHelpers.java @@ -21,6 +21,7 @@ package org.olat.course.nodes.members; import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Set; @@ -44,17 +45,33 @@ public class MembersHelpers { private MembersHelpers() { // CANNOT CREATE } + + private static void deduplicateList(List<Identity> list) { + if(list == null || list.size() < 2) return; + + Set<Identity> deduplicates = new HashSet<>(); + for(Iterator<Identity> it=list.iterator(); it.hasNext(); ) { + Identity identity = it.next(); + if(deduplicates.contains(identity)) { + it.remove(); + } else { + deduplicates.add(identity); + } + } + } // ----------------------------------------------------- public static List<Identity> getOwners(RepositoryService repositoryService, RepositoryEntry courseRepositoryEntry) { - return repositoryService.getMembers(courseRepositoryEntry, RepositoryEntryRelationType.all, GroupRoles.owner.name()); + List<Identity> owners = repositoryService.getMembers(courseRepositoryEntry, RepositoryEntryRelationType.all, GroupRoles.owner.name()); + deduplicateList(owners); + return owners; } // ----------------------------------------------------- - public static void addCoaches(ModuleConfiguration moduleConfiguration, CourseGroupManager cgm, BusinessGroupService bgs, List<Identity> list) { - + public static List<Identity> getCoaches(ModuleConfiguration moduleConfiguration, CourseGroupManager cgm, BusinessGroupService bgs) { + List<Identity> list = new ArrayList<>(); if(moduleConfiguration.has(MembersCourseNode.CONFIG_KEY_COACHES_GROUP)) { String coachGroupNames = moduleConfiguration.getStringValue(MembersCourseNode.CONFIG_KEY_COACHES_GROUP); List<Long> coachGroupKeys = moduleConfiguration.getList(MembersCourseNode.CONFIG_KEY_COACHES_GROUP_ID, Long.class); @@ -84,28 +101,31 @@ public class MembersHelpers { if(moduleConfiguration.anyTrue(MembersCourseNode.CONFIG_KEY_COACHES_ALL)) { list.addAll(retrieveCoachesFromCourseGroups(cgm)); } + + deduplicateList(list); + return list; } - public static List<Identity> retrieveCoachesFromAreas(List<Long> areaKeys, CourseGroupManager cgm) { + private static List<Identity> retrieveCoachesFromAreas(List<Long> areaKeys, CourseGroupManager cgm) { List<Identity> coaches = cgm.getCoachesFromAreas(areaKeys); return new ArrayList<>(new HashSet<>(coaches)); } - public static List<Identity> retrieveCoachesFromCurriculumElements(List<Long> elementKeys, CourseGroupManager cgm) { + private static List<Identity> retrieveCoachesFromCurriculumElements(List<Long> elementKeys, CourseGroupManager cgm) { List<Identity> coaches = cgm.getCoachesFromCurriculumElements(elementKeys); return new ArrayList<>(new HashSet<>(coaches)); } - public static List<Identity> retrieveCoachesFromGroups(List<Long> groupKeys, CourseGroupManager cgm) { + private static List<Identity> retrieveCoachesFromGroups(List<Long> groupKeys, CourseGroupManager cgm) { return new ArrayList<>(new HashSet<>(cgm.getCoachesFromBusinessGroups(groupKeys))); } - public static List<Identity> retrieveCoachesFromCourse(CourseGroupManager cgm) { + private static List<Identity> retrieveCoachesFromCourse(CourseGroupManager cgm) { return cgm.getCoaches(); } - public static List<Identity> retrieveCoachesFromCourseGroups(CourseGroupManager cgm) { + private static List<Identity> retrieveCoachesFromCourseGroups(CourseGroupManager cgm) { Set<Identity> uniq = new HashSet<>(); uniq.addAll(cgm.getCoachesFromAreas()); uniq.addAll(cgm.getCoachesFromBusinessGroups()); @@ -115,7 +135,8 @@ public class MembersHelpers { // ----------------------------------------------------- - public static void addParticipants(ModuleConfiguration moduleConfiguration, CourseGroupManager cgm, BusinessGroupService bgs, List<Identity> list) { + public static List<Identity> getParticipants(ModuleConfiguration moduleConfiguration, CourseGroupManager cgm, BusinessGroupService bgs) { + List<Identity> list = new ArrayList<>(); if(moduleConfiguration.has(MembersCourseNode.CONFIG_KEY_PARTICIPANTS_GROUP)) { String participantGroupNames = moduleConfiguration.getStringValue(MembersCourseNode.CONFIG_KEY_PARTICIPANTS_GROUP); @@ -147,21 +168,23 @@ public class MembersHelpers { if(moduleConfiguration.anyTrue(MembersCourseNode.CONFIG_KEY_PARTICIPANTS_ALL)) { list.addAll(retrieveParticipantsFromCourseGroups(cgm)); } + deduplicateList(list); + return list; } - public static List<Identity> retrieveParticipantsFromAreas(List<Long> areaKeys, CourseGroupManager cgm) { + private static List<Identity> retrieveParticipantsFromAreas(List<Long> areaKeys, CourseGroupManager cgm) { return cgm.getParticipantsFromAreas(areaKeys); } - public static List<Identity> retrieveParticipantsFromGroups(List<Long> groupKeys, CourseGroupManager cgm) { + private static List<Identity> retrieveParticipantsFromGroups(List<Long> groupKeys, CourseGroupManager cgm) { return cgm.getParticipantsFromBusinessGroups(groupKeys); } - public static List<Identity> retrieveParticipantsFromCurriculumElements(List<Long> elementKeys, CourseGroupManager cgm) { + private static List<Identity> retrieveParticipantsFromCurriculumElements(List<Long> elementKeys, CourseGroupManager cgm) { return cgm.getParticipantsFromCurriculumElements(elementKeys); } - public static List<Identity> retrieveParticipantsFromCourse(CourseGroupManager cgm) { + private static List<Identity> retrieveParticipantsFromCourse(CourseGroupManager cgm) { return cgm.getParticipants(); } diff --git a/src/main/java/org/olat/course/nodes/members/MembersPeekViewController.java b/src/main/java/org/olat/course/nodes/members/MembersPeekViewController.java index e948a2fb90e1c4c118cb54c9ec3f7a78d159b5c3..7da1838091c1785a1094998f376eee8f4ba90f44 100644 --- a/src/main/java/org/olat/course/nodes/members/MembersPeekViewController.java +++ b/src/main/java/org/olat/course/nodes/members/MembersPeekViewController.java @@ -127,14 +127,18 @@ public class MembersPeekViewController extends BasicController { owners = new ArrayList<>(); } - List<Identity> coaches = new ArrayList<>(); + List<Identity> coaches; if(withCoaches) { - MembersHelpers.addCoaches(config, cgm, businessGroupService, coaches); + coaches = MembersHelpers.getCoaches(config, cgm, businessGroupService); + } else { + coaches = new ArrayList<>(); } - List<Identity> participants = new ArrayList<>(); + List<Identity> participants; if(withParticipants) { - MembersHelpers.addParticipants(config, cgm, businessGroupService, participants); + participants = MembersHelpers.getParticipants(config, cgm, businessGroupService); + } else { + participants = new ArrayList<>(); } MembersCourseNodeConfiguration nodeConfig = (MembersCourseNodeConfiguration)CourseNodeFactory.getInstance().getCourseNodeConfiguration("cmembers"); diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java index 2afe7f4343e22ca7a1e2ca9c3860a783f8c94e9f..0233ba9be4bd8a056db26849bca425ee0291e466 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java @@ -30,6 +30,9 @@ 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.StringHelper; import org.olat.core.util.Util; import org.olat.core.util.vfs.VFSContainer; @@ -51,6 +54,7 @@ import org.olat.ims.qti21.model.xml.interactions.UploadAssessmentItemBuilder; import org.olat.ims.qti21.ui.AssessmentItemDisplayController; import org.olat.ims.qti21.ui.editor.events.AssessmentItemEvent; import org.olat.ims.qti21.ui.editor.events.DetachFromPoolEvent; +import org.olat.ims.qti21.ui.editor.events.SelectEvent.SelectionTarget; import org.olat.ims.qti21.ui.editor.interactions.ChoiceScoreController; import org.olat.ims.qti21.ui.editor.interactions.DrawingEditorController; import org.olat.ims.qti21.ui.editor.interactions.EssayEditorController; @@ -83,7 +87,7 @@ import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ -public class AssessmentItemEditorController extends BasicController { +public class AssessmentItemEditorController extends BasicController implements Activateable2 { private final AssessmentItemRef itemRef; private final ResolvedAssessmentItem resolvedAssessmentItem; @@ -521,6 +525,29 @@ public class AssessmentItemEditorController extends BasicController { return hottextItemBuilder; } + @Override + public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) { + if(entries == null || entries.isEmpty()) return; + + String type = entries.get(0).getOLATResourceable().getResourceableTypeName(); + if(SelectionTarget.description.name().equalsIgnoreCase(type) || SelectionTarget.expert.name().equalsIgnoreCase(type)) { + activate(ureq, itemEditor); + } else if(SelectionTarget.description.name().equalsIgnoreCase(type) || SelectionTarget.score.name().equalsIgnoreCase(type)) { + activate(ureq, scoreEditor); + } else if(SelectionTarget.feedback.name().equalsIgnoreCase(type)) { + activate(ureq, feedbackEditor); + } + } + + private void activate(UserRequest ureq, Controller ctrl) { + if(ctrl == null) return; + + int index = tabbedPane.indexOfTab(ctrl.getInitialComponent()); + if(index >= 0) { + tabbedPane.setSelectedPane(ureq, index); + } + } + @Override protected void event(UserRequest ureq, Component source, Event event) { if(tabbedPane == source) { diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java index ddf06f62955c65ead81087c91170330f98c425ed..f00371e4df89901ac3ba6671bc174db7e956f7c8 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java @@ -20,6 +20,7 @@ package org.olat.ims.qti21.ui.editor; import java.io.File; +import java.util.List; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; @@ -29,10 +30,14 @@ 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.Util; import org.olat.core.util.vfs.VFSContainer; import org.olat.ims.qti21.ui.AssessmentTestDisplayController; import org.olat.ims.qti21.ui.editor.events.AssessmentSectionEvent; +import org.olat.ims.qti21.ui.editor.events.SelectEvent.SelectionTarget; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; @@ -42,7 +47,7 @@ import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ -public class AssessmentSectionEditorController extends BasicController { +public class AssessmentSectionEditorController extends BasicController implements Activateable2 { private final TabbedPane tabbedPane; private final VelocityContainer mainVC; @@ -95,6 +100,18 @@ public class AssessmentSectionEditorController extends BasicController { // } + @Override + public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) { + if(entries == null || entries.isEmpty()) return; + + String type = entries.get(0).getOLATResourceable().getResourceableTypeName(); + if(SelectionTarget.description.name().equalsIgnoreCase(type) || SelectionTarget.score.name().equalsIgnoreCase(type)) { + tabbedPane.setSelectedPane(ureq, tabbedPane.indexOfTab(optionsCtrl.getInitialComponent())); + } else if(SelectionTarget.expert.name().equalsIgnoreCase(type)) { + tabbedPane.setSelectedPane(ureq, tabbedPane.indexOfTab(expertOptionsCtrl.getInitialComponent())); + } + } + @Override protected void event(UserRequest ureq, Component source, Event event) { // diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java index 6ed1ed876218d146a544fb04ca07213d052b31cf..b49f62f1ca5ff72711f1613eafdaf0773867b2f7 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java @@ -59,6 +59,7 @@ import org.olat.core.gui.control.VetoableCloseController; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.MainLayoutBasicController; import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController; +import org.olat.core.gui.control.generic.dtabs.Activateable2; import org.olat.core.gui.control.generic.messages.MessageController; import org.olat.core.gui.control.generic.messages.MessageUIFactory; import org.olat.core.gui.control.generic.modal.DialogBoxController; @@ -70,12 +71,16 @@ import org.olat.core.gui.control.generic.wizard.StepsRunContext; import org.olat.core.gui.media.MediaResource; import org.olat.core.helpers.Settings; import org.olat.core.id.Roles; +import org.olat.core.id.context.BusinessControlFactory; +import org.olat.core.id.context.ContextEntry; import org.olat.core.logging.AssertException; import org.olat.core.logging.activity.ThreadLocalUserActivityLogger; import org.olat.core.util.Formatter; import org.olat.core.util.Util; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.core.util.coordinate.LockResult; +import org.olat.core.util.resource.OresHelper; +import org.olat.core.util.tree.TreeHelper; import org.olat.core.util.vfs.VFSContainer; import org.olat.fileresource.FileResourceManager; import org.olat.ims.qti21.AssessmentTestHelper; @@ -114,6 +119,9 @@ import org.olat.ims.qti21.ui.editor.events.AssessmentSectionEvent; import org.olat.ims.qti21.ui.editor.events.AssessmentTestEvent; import org.olat.ims.qti21.ui.editor.events.AssessmentTestPartEvent; import org.olat.ims.qti21.ui.editor.events.DetachFromPoolEvent; +import org.olat.ims.qti21.ui.editor.events.SelectEvent; +import org.olat.ims.qti21.ui.editor.events.SelectEvent.SelectionTarget; +import org.olat.ims.qti21.ui.editor.overview.AssessmentTestOverviewConfigurationController; import org.olat.imscp.xml.manifest.FileType; import org.olat.imscp.xml.manifest.ResourceType; import org.olat.modules.qpool.QuestionItemFull; @@ -132,6 +140,7 @@ import uk.ac.ed.ph.jqtiplus.node.test.AbstractPart; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; +import uk.ac.ed.ph.jqtiplus.node.test.ControlObject; import uk.ac.ed.ph.jqtiplus.node.test.SectionPart; import uk.ac.ed.ph.jqtiplus.node.test.TestPart; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; @@ -160,6 +169,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController newEssayLink, newUploadLink, newDrawingLink; private Link importFromPoolLink, importFromTableLink, exportToPoolLink, exportToDocxLink; private Link reloadInCacheLink, deleteLink, copyLink; + private Link configurationOverviewLink; private final TooledStackedPanel toolbar; private VelocityContainer mainVC; @@ -169,6 +179,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController private DialogBoxController confirmDeleteCtrl; private StepsMainRunController importTableWizard; private LayoutMain3ColsController columnLayoutCtr; + private AssessmentTestOverviewConfigurationController overviewConfigCtrl; private File unzippedDirRoot; private VFSContainer unzippedContRoot; @@ -352,6 +363,9 @@ public class AssessmentTestComposerController extends MainLayoutBasicController copyLink.setDomReplacementWrapperRequired(false); changeItemTools.addComponent(copyLink); + configurationOverviewLink = LinkFactory.createToolLink("configuration.overview", translate("configuration.overview"), this, "o_icon_description"); + configurationOverviewLink.setDomReplacementWrapperRequired(false); + // main layout mainVC = createVelocityContainer("assessment_test_composer"); columnLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), menuTree, mainVC, "at" + testEntry.getKey()); @@ -413,6 +427,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController toolbar.addTool(exportItemTools, Align.left); toolbar.addTool(addItemTools, Align.left); toolbar.addTool(changeItemTools, Align.left); + toolbar.addTool(configurationOverviewLink, Align.right); } @Override @@ -476,6 +491,14 @@ public class AssessmentTestComposerController extends MainLayoutBasicController doDelete(ureq, (TreeNode)confirmDeleteCtrl.getUserObject()); } cleanUp(); + } else if(overviewConfigCtrl == source) { + if(event instanceof SelectEvent) { + SelectEvent se = (SelectEvent)event; + if(doSelect(ureq, se.getControlObject(), se.getTarget())) { + cleanUp(); + } + } + } else if(cmc == source) { cleanUp(); } @@ -483,9 +506,11 @@ public class AssessmentTestComposerController extends MainLayoutBasicController } private void cleanUp() { + removeAsListenerAndDispose(overviewConfigCtrl); removeAsListenerAndDispose(confirmDeleteCtrl); removeAsListenerAndDispose(selectQItemCtrl); removeAsListenerAndDispose(cmc); + overviewConfigCtrl = null; confirmDeleteCtrl = null; selectQItemCtrl = null; cmc = null; @@ -551,7 +576,25 @@ public class AssessmentTestComposerController extends MainLayoutBasicController doCopy(ureq); } else if(reloadInCacheLink == source) { doForceReloadFiles(ureq); + } else if(configurationOverviewLink == source) { + doConfigurationOverview(ureq); + } + } + + private boolean doSelect(UserRequest ureq, ControlObject<?> uobject, SelectionTarget target) { + TreeNode selectedNode = TreeHelper.findNodeByUserObject(uobject, menuTree.getTreeModel().getRootNode()); + if(selectedNode != null) { + toolbar.popUpToController(this); + + partEditorFactory(ureq, selectedNode); + if(currentEditorCtrl instanceof Activateable2) { + List<ContextEntry> entries = BusinessControlFactory.getInstance() + .createCEListFromString(OresHelper.createOLATResourceableType(target.name())); + ((Activateable2)currentEditorCtrl).activate(ureq, entries, null); + } + return currentEditorCtrl != null; } + return false; } private void doSelectQItem(UserRequest ureq) { @@ -1306,6 +1349,15 @@ public class AssessmentTestComposerController extends MainLayoutBasicController assessmentChanged(ureq); } + private void doConfigurationOverview(UserRequest ureq) { + removeAsListenerAndDispose(overviewConfigCtrl); + + overviewConfigCtrl = new AssessmentTestOverviewConfigurationController(ureq, getWindowControl(), toolbar, + testEntry, resolvedAssessmentTest); + listenTo(overviewConfigCtrl); + toolbar.pushController(translate("configuration.overview"), overviewConfigCtrl); + } + private void doConfirmDelete(UserRequest ureq) { if(confirmDeleteCtrl != null) return; diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestEditorController.java index 482b0cbe2c367236fdaf8419dc7d1958b122ab06..c8a206b73926fee6552ca4103ce0111bea4e0675 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestEditorController.java @@ -20,6 +20,7 @@ package org.olat.ims.qti21.ui.editor; import java.io.File; +import java.util.List; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; @@ -29,11 +30,15 @@ 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.Util; import org.olat.core.util.vfs.VFSContainer; import org.olat.ims.qti21.model.xml.AssessmentTestBuilder; import org.olat.ims.qti21.ui.AssessmentTestDisplayController; import org.olat.ims.qti21.ui.editor.events.AssessmentTestEvent; +import org.olat.ims.qti21.ui.editor.events.SelectEvent.SelectionTarget; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; import uk.ac.ed.ph.jqtiplus.node.test.TestPart; @@ -44,7 +49,7 @@ import uk.ac.ed.ph.jqtiplus.node.test.TestPart; * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ -public class AssessmentTestEditorController extends BasicController { +public class AssessmentTestEditorController extends BasicController implements Activateable2 { private final TabbedPane tabbedPane; private final VelocityContainer mainVC; @@ -113,6 +118,29 @@ public class AssessmentTestEditorController extends BasicController { } } + @Override + public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) { + if(entries == null || entries.isEmpty()) return; + + String type = entries.get(0).getOLATResourceable().getResourceableTypeName(); + if(SelectionTarget.description.name().equalsIgnoreCase(type) || SelectionTarget.score.name().equalsIgnoreCase(type)) { + activate(ureq, optionsCtrl); + } else if(SelectionTarget.expert.name().equalsIgnoreCase(type) && testPartOptionsCtrl != null) { + activate(ureq, testPartOptionsCtrl); + } else if(SelectionTarget.feedback.name().equalsIgnoreCase(type) && testPartOptionsCtrl != null) { + activate(ureq, testPartOptionsCtrl); + } + } + + private void activate(UserRequest ureq, Controller ctrl) { + if(ctrl == null) return; + + int index = tabbedPane.indexOfTab(ctrl.getInitialComponent()); + if(index >= 0) { + tabbedPane.setSelectedPane(ureq, index); + } + } + @Override protected void event(UserRequest ureq, Component source, Event event) { // diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties index e956dc4cb83a794acca1d6dd54ed68176b668fd3..2ecf967b5524a1927bccb08e2a1ae987e7e91523 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties @@ -15,6 +15,10 @@ add.match.column=Kolonne hinzuf\u00FCgen add.match.row=Zeile hinzuf\u00FCgen answers=Antworten change.elements=Elemente \u00E4ndern +configuration.overview=\u00DCbersicht Testkonfiguration +configuration.option.yes=Ja +configuration.option.no=Nein +configuration.option.inherited=Vererbt convert=Konvertieren convert.alien=Konvertieren convert.to=Umwandeln in\: @@ -139,7 +143,9 @@ form.imd.wrong.kprim=Falsch form.kprim=Kprim form.match=Match form.metadata=Metadaten +form.metadata.creationDate=Erstellt form.metadata.description=Beschreibung +form.metadata.initialAuthor=Erstellt durch form.metadata.title=Titel form.pool=Fragenpool form.score=Punkte @@ -228,6 +234,16 @@ new.upload=Datei hochladen preview=Vorschau preview.solution=Vorschau L\u00F6sung rights.owners=$org.olat.modules.qpool.ui\:rights.owners +table.header.attempts=L\u00F6sungsversuche +table.header.comment=Kommentar erlauben +table.header.feedback=Feedback +table.header.identifier=Frage ID +table.header.points=Punkte +table.header.review=R\u00FCckblick erlauben +table.header.skipping=\u00DCberspring erlauben +table.header.solution=L\u00F6sung anzeigen +table.header.title=Titel +table.header.type=Fragetyp time.limit.max=Zeitbeschr\u00E4nkung title.add=$org.olat.ims.qti.editor\:title.add tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties index 5cc01e68a949828a60557cc71f5e7ede51cbf18e..3e0897cca8c9bcb91f55028bf5c29f80635e2cc5 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties @@ -15,6 +15,10 @@ add.match.column=Add column add.match.row=Add row answers=Answers change.elements=Edition +configuration.overview=Test configuration overview +configuration.option.yes=Yes +configuration.option.no=No +configuration.option.inherited=Inherited convert=Convert convert.alien=Convert convert.to=Convert to\: @@ -139,7 +143,9 @@ form.imd.wrong.kprim=False form.kprim=Kprim form.match=Match form.metadata=Metadata +form.metadata.creationDate=Created form.metadata.description=Description +form.metadata.initialAuthor=Creator form.metadata.title=Title form.pool=Question bank form.score=Score @@ -228,6 +234,16 @@ new.upload=Upload file preview=Preview preview.solution=Preview solution rights.owners=$org.olat.modules.qpool.ui\:rights.owners +table.header.attempts=Attempts +table.header.comment=Allow comment +table.header.identifier=Question ID +table.header.feedback=Feedback +table.header.points=Points +table.header.review=Allow review +table.header.skipping=Allow skipping +table.header.solution=Show solution +table.header.title=Title +table.header.type=Type time.limit.max=Time limit title.add=$org.olat.ims.qti.editor\:title.add tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/events/SelectEvent.java b/src/main/java/org/olat/ims/qti21/ui/editor/events/SelectEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..a62ff48cbf196e9f2ba1adbcbac313b8e76dd7f7 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/events/SelectEvent.java @@ -0,0 +1,63 @@ +/** + * <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.editor.events; + +import org.olat.core.gui.control.Event; + +import uk.ac.ed.ph.jqtiplus.node.test.ControlObject; + +/** + * + * Initial date: 15 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class SelectEvent extends Event { + + private static final long serialVersionUID = 1554430878603467523L; + public static final String SELECT = "select-control-object"; + + private final SelectionTarget target; + private final ControlObject<?> controlObject; + + public SelectEvent(ControlObject<?> controlObject, SelectionTarget target) { + super(SELECT); + this.target = target; + this.controlObject = controlObject; + } + + public SelectionTarget getTarget() { + return target; + } + + public ControlObject<?> getControlObject() { + return controlObject; + } + + public enum SelectionTarget { + + description, + expert, + score, + feedback + + } + +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentSectionScoreCellRenderer.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentSectionScoreCellRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..46ac4c1398de683a64f67aae4323fcf879fd94cd --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentSectionScoreCellRenderer.java @@ -0,0 +1,59 @@ +/** + * <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.editor.overview; + +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiCellRenderer; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableComponent; +import org.olat.core.gui.components.form.flexible.impl.elements.table.StaticFlexiCellRenderer; +import org.olat.core.gui.render.Renderer; +import org.olat.core.gui.render.StringOutput; +import org.olat.core.gui.render.URLBuilder; +import org.olat.core.gui.translator.Translator; +import org.olat.modules.assessment.ui.ScoreCellRenderer; + +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; + +/** + * + * Initial date: 16 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class AssessmentSectionScoreCellRenderer implements FlexiCellRenderer { + + private final ScoreCellRenderer scoreRenderer; + private final StaticFlexiCellRenderer actionRenderer; + + public AssessmentSectionScoreCellRenderer(String action) { + scoreRenderer = new ScoreCellRenderer(); + actionRenderer = new StaticFlexiCellRenderer(action, scoreRenderer); + } + + @Override + public void render(Renderer renderer, StringOutput target, Object cellValue, int row, FlexiTableComponent source, + URLBuilder ubu, Translator translator) { + ControlObjectRow object = (ControlObjectRow)source.getFlexiTableElement().getTableDataModel().getObject(row); + if(object.getControlObject() instanceof AssessmentSection) { + scoreRenderer.render(renderer, target, cellValue, row, source, ubu, translator); + } else { + actionRenderer.render(renderer, target, cellValue, row, source, ubu, translator); + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewConfigurationController.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewConfigurationController.java new file mode 100644 index 0000000000000000000000000000000000000000..7e920e555671f22b3c12e5dc7636200d1bae557c --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewConfigurationController.java @@ -0,0 +1,321 @@ +/** + * <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.editor.overview; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.DoubleAdder; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItem; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.DateChooser; +import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement; +import org.olat.core.gui.components.form.flexible.elements.TextElement; +import org.olat.core.gui.components.form.flexible.impl.FormBasicController; +import org.olat.core.gui.components.form.flexible.impl.FormEvent; +import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; +import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiCellRenderer; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableComponent; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory; +import org.olat.core.gui.components.form.flexible.impl.elements.table.SelectionEvent; +import org.olat.core.gui.components.form.flexible.impl.elements.table.StaticFlexiCellRenderer; +import org.olat.core.gui.components.stack.TooledStackedPanel; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.gui.render.Renderer; +import org.olat.core.gui.render.StringOutput; +import org.olat.core.gui.render.URLBuilder; +import org.olat.core.gui.translator.Translator; +import org.olat.core.util.StringHelper; +import org.olat.core.util.Util; +import org.olat.course.assessment.AssessmentHelper; +import org.olat.ims.qti21.model.xml.QtiNodesExtractor; +import org.olat.ims.qti21.ui.assessment.components.QuestionTypeFlexiCellRenderer; +import org.olat.ims.qti21.ui.editor.AssessmentTestComposerController; +import org.olat.ims.qti21.ui.editor.events.SelectEvent; +import org.olat.ims.qti21.ui.editor.events.SelectEvent.SelectionTarget; +import org.olat.ims.qti21.ui.editor.overview.AssessmentTestOverviewDataModel.PartCols; +import org.olat.repository.RepositoryEntry; +import org.olat.user.UserManager; +import org.springframework.beans.factory.annotation.Autowired; + +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; +import uk.ac.ed.ph.jqtiplus.node.test.SectionPart; +import uk.ac.ed.ph.jqtiplus.node.test.TestPart; +import uk.ac.ed.ph.jqtiplus.node.test.TimeLimits; +import uk.ac.ed.ph.jqtiplus.provision.BadResourceException; +import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; +import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest; + +/** + * + * Initial date: 15 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class AssessmentTestOverviewConfigurationController extends FormBasicController { + + private FlexiTableElement tableEl; + private AssessmentTestOverviewDataModel tableModel; + private final TooledStackedPanel toolbar; + + private final RepositoryEntry testEntry; + private final ResolvedAssessmentTest resolvedAssessmentTest; + + @Autowired + private UserManager userManager; + + public AssessmentTestOverviewConfigurationController(UserRequest ureq, WindowControl wControl, TooledStackedPanel toolbar, + RepositoryEntry testEntry, ResolvedAssessmentTest resolvedAssessmentTest) { + super(ureq, wControl, "overview", Util.createPackageTranslator(AssessmentTestComposerController.class, ureq.getLocale())); + this.testEntry = testEntry; + this.resolvedAssessmentTest = resolvedAssessmentTest; + this.toolbar = toolbar; + initForm(ureq); + initialPanel.setCssClass("o_edit_mode"); + loadModel(); + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + initInfosForm(formLayout); + initTableForm(formLayout, ureq); + } + + private void initInfosForm(FormItemContainer formLayout) { + FormLayoutContainer infosLayout = FormLayoutContainer.createDefaultFormLayout("test.infos", getTranslator()); + formLayout.add("infos", infosLayout); + infosLayout.setRootForm(mainForm); + + AssessmentTest assessmentTest = resolvedAssessmentTest.getRootNodeLookup().extractIfSuccessful(); + + String title = assessmentTest.getTitle(); + TextElement titleEl = uifactory.addTextElement("title", "form.metadata.title", 255, title, infosLayout); + titleEl.setEnabled(false); + + String initalAuthor = testEntry.getInitialAuthor() == null ? "-" : testEntry.getInitialAuthor(); + if(testEntry.getInitialAuthor() != null) { + initalAuthor = userManager.getUserDisplayName(initalAuthor); + } + TextElement creatorEl = uifactory.addTextElement("initialAuthor", "form.metadata.initialAuthor", 255, StringHelper.escapeHtml(initalAuthor), infosLayout); + creatorEl.setEnabled(false); + DateChooser creationEl = uifactory.addDateChooser("form.metadata.creationDate", testEntry.getCreationDate(), infosLayout); + creationEl.setEnabled(false); + Double maxScore = QtiNodesExtractor.extractMaxScore(assessmentTest); + String maxScoreStr = AssessmentHelper.getRoundedScore(maxScore); + TextElement maxScoreEl = uifactory.addTextElement("max.score", "max.score", 255, maxScoreStr, infosLayout); + maxScoreEl.setEnabled(false); + Double cutValue = QtiNodesExtractor.extractCutValue(assessmentTest); + String cutValueStr = AssessmentHelper.getRoundedScore(cutValue); + TextElement cutValueEl = uifactory.addTextElement("cut.value", "cut.value", 255, cutValueStr, infosLayout); + cutValueEl.setEnabled(false); + + TimeLimits timeLimits = assessmentTest.getTimeLimits(); + + long maxInSeconds = -1; + String timeMaxHour = ""; + String timeMaxMinute = ""; + if(timeLimits != null && timeLimits.getMaximum() != null && timeLimits.getMaximum().longValue() > 0) { + maxInSeconds = timeLimits.getMaximum().longValue(); + timeMaxHour = Long.toString(maxInSeconds / 3600); + timeMaxMinute = Long.toString((maxInSeconds % 3600) / 60); + } + + if(StringHelper.containsNonWhitespace(timeMaxHour) || StringHelper.containsNonWhitespace(timeMaxMinute)) { + String page = velocity_root + "/max_time_limit.html"; + FormLayoutContainer maxTimeCont = FormLayoutContainer.createCustomFormLayout("time.limit.cont", getTranslator(), page); + infosLayout.add(maxTimeCont); + maxTimeCont.setLabel("time.limit.max", null); + + timeMaxHour = timeMaxHour.equals("0") ? "" : timeMaxHour; + TextElement maxTimeHourEl = uifactory.addTextElement("time.limit.hour", "time.limit.max", 4, timeMaxHour, maxTimeCont); + maxTimeHourEl.setDomReplacementWrapperRequired(false); + maxTimeHourEl.setDisplaySize(4); + maxTimeHourEl.setEnabled(false); + + TextElement maxTimeMinuteEl = uifactory.addTextElement("time.limit.minute", "time.limit.max", 4, timeMaxMinute, maxTimeCont); + maxTimeMinuteEl.setDomReplacementWrapperRequired(false); + maxTimeMinuteEl.setDisplaySize(4); + maxTimeMinuteEl.setEnabled(false); + } + } + + private void initTableForm(FormItemContainer formLayout, UserRequest ureq) { + FlexiTableColumnModel tableColumnModel = FlexiTableDataModelFactory.createFlexiTableColumnModel(); + + DefaultFlexiColumnModel titleCol = new DefaultFlexiColumnModel(PartCols.title, + SelectionTarget.description.name(), new HierarchicalPartCellRenderer()); + titleCol.setAlwaysVisible(true); + tableColumnModel.addFlexiColumnModel(titleCol); + tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(PartCols.maxScore, + new AssessmentSectionScoreCellRenderer(SelectionTarget.score.name()))); + tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(PartCols.attempts, + SelectionTarget.expert.name(), new MaxAttemptsCellRenderer(getTranslator()))); + tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(PartCols.skipping, + new TestAndSectionCellRenderer(SelectionTarget.expert.name(), new OptionCellRenderer(getTranslator())))); + tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(PartCols.comment, + new TestAndSectionCellRenderer(SelectionTarget.expert.name(), new OptionCellRenderer(getTranslator())))); + tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(PartCols.review, + new TestAndSectionCellRenderer(SelectionTarget.expert.name(), new OptionCellRenderer(getTranslator())))); + tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(PartCols.solution, + new TestAndSectionCellRenderer(SelectionTarget.expert.name(), new OptionCellRenderer(getTranslator())))); + tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, PartCols.type, new QuestionTypeFlexiCellRenderer(getTranslator()))); + tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, PartCols.identifier)); + tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, PartCols.feedback, SelectionTarget.feedback.name())); + + tableModel = new AssessmentTestOverviewDataModel(tableColumnModel); + + tableEl = uifactory.addTableElement(getWindowControl(), "overview", tableModel, 32000, false, getTranslator(), formLayout); + tableEl.setElementCssClass("o_sel_qti_configuration_overview"); + tableEl.setExportEnabled(true); + tableEl.setAndLoadPersistedPreferences(ureq, "assessment-test-config-overview"); + } + + @Override + protected void doDispose() { + toolbar.setCssClass(""); + } + + private void loadModel() { + List<ControlObjectRow> rows = new ArrayList<>(); + + AssessmentTest test = resolvedAssessmentTest.getTestLookup().getRootNodeHolder().getRootNode(); + rows.add(ControlObjectRow.valueOf(test)); + + List<TestPart> parts = test.getTestParts(); + if(parts.size() == 1) { + List<AssessmentSection> sections = parts.get(0).getAssessmentSections(); + for(AssessmentSection section:sections) { + loadModel(section, rows); + } + } else { + for(int i=0; i<parts.size(); i++) { + loadModel(parts.get(i), (i+1), rows); + } + } + + tableModel.setObjects(rows); + tableEl.reset(true, true, true); + } + + private void loadModel(TestPart part, int pos, List<ControlObjectRow> rows) { + rows.add(ControlObjectRow.valueOf(part, pos)); + List<AssessmentSection> sections = part.getAssessmentSections(); + for(AssessmentSection section:sections) { + loadModel(section, rows); + } + } + + private Double loadModel(AssessmentSection section, List<ControlObjectRow> rows) { + ControlObjectRow sectionRow = ControlObjectRow.valueOf(section); + rows.add(sectionRow); + + boolean someMaxScore = false; + DoubleAdder atomicMaxScore = new DoubleAdder(); + for(SectionPart part: section.getSectionParts()) { + + Double maxScore = null; + if(part instanceof AssessmentItemRef) { + maxScore = loadModel((AssessmentItemRef)part, rows); + } else if(part instanceof AssessmentSection) { + maxScore = loadModel((AssessmentSection) part, rows); + } + + if(maxScore != null) { + someMaxScore = true; + atomicMaxScore.add(maxScore.doubleValue()); + } + } + + if(someMaxScore) { + sectionRow.setMaxScore(Double.valueOf(atomicMaxScore.sum())); + } + return sectionRow.getMaxScore(); + } + + private Double loadModel(AssessmentItemRef itemRef, List<ControlObjectRow> rows) { + Double maxScore = null; + ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef); + if(resolvedAssessmentItem == null || resolvedAssessmentItem.getItemLookup() == null + || resolvedAssessmentItem.getItemLookup().getRootNodeHolder() == null) { + rows.add(ControlObjectRow.errorOf(itemRef)); + } else { + BadResourceException ex = resolvedAssessmentItem.getItemLookup().getBadResourceException(); + if(ex != null) { + rows.add(ControlObjectRow.errorOf(itemRef)); + } else { + AssessmentItem assessmentItem = resolvedAssessmentItem.getItemLookup().getRootNodeHolder().getRootNode(); + ControlObjectRow row = ControlObjectRow.valueOf(itemRef, assessmentItem); + maxScore = row.getMaxScore(); + rows.add(row); + + } + } + return maxScore; + } + + @Override + protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { + if(tableEl == source) { + if(event instanceof SelectionEvent) { + SelectionEvent se = (SelectionEvent)event; + if(StringHelper.containsNonWhitespace(se.getCommand())) { + SelectionTarget target = SelectionTarget.valueOf(se.getCommand()); + ControlObjectRow row = tableModel.getObject(se.getIndex()); + fireEvent(ureq, new SelectEvent(row.getControlObject(), target)); + } + } + } + super.formInnerEvent(ureq, source, event); + } + + @Override + protected void formOK(UserRequest ureq) { + // + } + + private static class TestAndSectionCellRenderer implements FlexiCellRenderer { + + private final FlexiCellRenderer delegate; + private final StaticFlexiCellRenderer actionRenderer; + + public TestAndSectionCellRenderer(String action, FlexiCellRenderer delegate) { + this.delegate = delegate; + actionRenderer = new StaticFlexiCellRenderer(action, delegate); + } + + @Override + public void render(Renderer renderer, StringOutput target, Object cellValue, int row, + FlexiTableComponent source, URLBuilder ubu, Translator translator) { + ControlObjectRow object = (ControlObjectRow)source.getFlexiTableElement().getTableDataModel().getObject(row); + if(object.getControlObject() instanceof AssessmentItemRef) { + delegate.render(renderer, target, cellValue, row, source, ubu, translator); + } else { + actionRenderer.render(renderer, target, cellValue, row, source, ubu, translator); + } + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewDataModel.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewDataModel.java new file mode 100644 index 0000000000000000000000000000000000000000..0e95d2ebfbd25ff0593bfd732cd97abab5b4aa3a --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewDataModel.java @@ -0,0 +1,95 @@ +/** + * <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.editor.overview; + +import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableDataModel; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiSortableColumnDef; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel; + +/** + * + * Initial date: 15 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class AssessmentTestOverviewDataModel extends DefaultFlexiTableDataModel<ControlObjectRow> { + + public AssessmentTestOverviewDataModel(FlexiTableColumnModel columnModel) { + super(columnModel); + } + + @Override + public Object getValueAt(int row, int col) { + ControlObjectRow partRow = getObject(row); + switch(PartCols.values()[col]) { + case title: return partRow; + case maxScore: return partRow.getMaxScore(); + case attempts: return partRow.getAttemptOption(); + case skipping: return partRow.getSkipping(); + case comment: return partRow.getComment(); + case review: return partRow.getReview(); + case solution: return partRow.getSolution(); + case type: return partRow.getType(); + case identifier: return partRow.getIdentifier(); + case feedback: return partRow.getFeedbacks(); + default: return "ERROR"; + } + } + + @Override + public AssessmentTestOverviewDataModel createCopyWithEmptyList() { + return new AssessmentTestOverviewDataModel(getTableColumnModel()); + } + + public enum PartCols implements FlexiSortableColumnDef { + + title("table.header.title"), + maxScore("table.header.points"), + attempts("table.header.attempts"), + skipping("table.header.skipping"), + comment("table.header.comment"), + review("table.header.review"), + solution("table.header.solution"), + type("table.header.type"), + identifier("table.header.identifier"), + feedback("table.header.feedback"); + + private final String i18nKey; + + private PartCols(String i18nKey) { + this.i18nKey = i18nKey; + } + + @Override + public String i18nHeaderKey() { + return i18nKey; + } + + @Override + public boolean sortable() { + return false; + } + + @Override + public String sortKey() { + return name(); + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/ControlObjectRow.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/ControlObjectRow.java new file mode 100644 index 0000000000000000000000000000000000000000..52fa818358f8db798ffa8c233d84a5d7001b974f --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/ControlObjectRow.java @@ -0,0 +1,231 @@ +/** + * <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.editor.overview; + +import java.util.List; + +import org.olat.ims.qti21.model.QTI21QuestionType; +import org.olat.ims.qti21.model.xml.QtiNodesExtractor; + +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.test.AbstractPart; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; +import uk.ac.ed.ph.jqtiplus.node.test.ControlObject; +import uk.ac.ed.ph.jqtiplus.node.test.ItemSessionControl; +import uk.ac.ed.ph.jqtiplus.node.test.TestPart; + +/** + * + * Initial date: 15 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class ControlObjectRow { + + private final String title; + private final String iconCssClass; + private final ControlObject<?> part; + + private Double maxScore; + private Boolean feedbacks; + private QTI21QuestionType type; + + private OptionEnum review; + private OptionEnum comment; + private OptionEnum skipping; + private OptionEnum solution; + private MaxAttemptOption attemptOption; + + public ControlObjectRow(String title, ControlObject<?> part, String iconCssClass) { + this.part = part; + this.title = title; + this.iconCssClass = iconCssClass; + } + + public static ControlObjectRow valueOf(AssessmentTest assessmentTest) { + ControlObjectRow row = new ControlObjectRow(assessmentTest.getTitle(), assessmentTest, "o_qtiassessment_icon"); + row.maxScore = QtiNodesExtractor.extractMaxScore(assessmentTest); + + boolean hasFeedbacks = !assessmentTest.getTestFeedbacks().isEmpty(); + row.feedbacks = Boolean.valueOf(hasFeedbacks); + + List<TestPart> parts = assessmentTest.getTestParts(); + if(parts.size() == 1) { + TestPart part = parts.get(0); + configuration(row, part); + } + return row; + } + + public static ControlObjectRow valueOf(TestPart part, int pos) { + ControlObjectRow row = new ControlObjectRow(pos + ". Test part", part, "o_icon-lg o_qtiassessment_icon"); + configuration(row, part); + return row; + } + + public static ControlObjectRow valueOf(AssessmentSection section) { + ControlObjectRow row = new ControlObjectRow(section.getTitle(), section, "o_mi_qtisection"); + configuration(row, section); + return row; + } + + public static ControlObjectRow errorOf(AssessmentItemRef itemRef) { + return new ControlObjectRow("ERROR", itemRef, "o_icon_error"); + } + + public static ControlObjectRow valueOf(AssessmentItemRef itemRef, AssessmentItem assessmentItem) { + String itemCssClass; + QTI21QuestionType type = QTI21QuestionType.getType(assessmentItem); + if(type != null) { + itemCssClass = type.getCssClass(); + } else { + itemCssClass = "o_mi_qtiunkown"; + } + ControlObjectRow row = new ControlObjectRow(assessmentItem.getTitle(), itemRef, itemCssClass); + row.maxScore = QtiNodesExtractor.extractMaxScore(assessmentItem); + row.type = type; + boolean hasFeedbacks = !assessmentItem.getModalFeedbacks().isEmpty(); + row.feedbacks = Boolean.valueOf(hasFeedbacks); + configuration(row, itemRef); + return row; + } + + private static void configuration(ControlObjectRow row, AbstractPart part) { + attempts(row, part); + skipping(row, part); + comment(row, part); + review(row, part); + solution(row, part); + } + + private static void attempts(ControlObjectRow row, AbstractPart part) { + Integer maxAttempts = null; + ItemSessionControl itemSessionControl = part.getItemSessionControl(); + if(itemSessionControl != null) { + maxAttempts = itemSessionControl.getMaxAttempts(); + } + + if(maxAttempts != null) { + OptionEnum option = (maxAttempts.intValue() == 0 ? OptionEnum.no : OptionEnum.yes); + row.attemptOption = MaxAttemptOption.valueOf(option, maxAttempts); + } else { + OptionEnum option = (part instanceof TestPart) ? OptionEnum.no : OptionEnum.inherited; + row.attemptOption = MaxAttemptOption.valueOf(option, Integer.valueOf(1)); + } + } + + private static void skipping(ControlObjectRow row, AbstractPart part) { + ItemSessionControl itemSessionControl = part.getItemSessionControl(); + if(itemSessionControl != null && itemSessionControl.getAllowSkipping() != null) { + row.skipping = itemSessionControl.getAllowSkipping().booleanValue() ? OptionEnum.yes : OptionEnum.no; + } else { + row.skipping = (part instanceof TestPart) ? OptionEnum.yes : OptionEnum.inherited; + } + } + + private static void comment(ControlObjectRow row, AbstractPart part) { + ItemSessionControl itemSessionControl = part.getItemSessionControl(); + if(itemSessionControl != null && itemSessionControl.getAllowComment() != null) { + row.comment = itemSessionControl.getAllowComment().booleanValue() ? OptionEnum.yes : OptionEnum.no; + } else { + row.comment = (part instanceof TestPart) ? OptionEnum.yes : OptionEnum.inherited; + } + } + + private static void review(ControlObjectRow row, AbstractPart part) { + ItemSessionControl itemSessionControl = part.getItemSessionControl(); + if(itemSessionControl != null && itemSessionControl.getAllowReview() != null) { + row.review = itemSessionControl.getAllowReview().booleanValue() ? OptionEnum.yes : OptionEnum.no; + } else { + row.review = (part instanceof TestPart) ? OptionEnum.no : OptionEnum.inherited; + } + } + + private static void solution(ControlObjectRow row, AbstractPart part) { + ItemSessionControl itemSessionControl = part.getItemSessionControl(); + if(itemSessionControl != null && itemSessionControl.getShowSolution() != null) { + row.solution = itemSessionControl.getShowSolution().booleanValue() ? OptionEnum.yes : OptionEnum.no; + } else { + row.solution = (part instanceof TestPart) ? OptionEnum.no : OptionEnum.inherited; + } + } + + public String getTitle() { + return title; + } + + public String getIdentifier() { + return part.getIdentifier().toString(); + } + + public ControlObject<?> getControlObject() { + return part; + } + + public String getIconCssClass() { + return iconCssClass; + } + + public Double getMaxScore() { + return maxScore; + } + + public void setMaxScore(Double maxScore) { + this.maxScore = maxScore; + } + + public QTI21QuestionType getType() { + return type; + } + + public Boolean getFeedbacks() { + return feedbacks; + } + + public OptionEnum getSkipping() { + return skipping; + } + + public OptionEnum getComment() { + return comment; + } + + public OptionEnum getReview() { + return review; + } + + public OptionEnum getSolution() { + return solution; + } + + public MaxAttemptOption getAttemptOption() { + return attemptOption; + } + + public int getDepth() { + int depth = 0; + for(ControlObject<?> parent=part.getParent(); parent != null; parent = parent.getParent()) { + depth++; + } + return depth; + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/HierarchicalPartCellRenderer.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/HierarchicalPartCellRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..2341a6f8b4765746db011bca131f39d0788d35ae --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/HierarchicalPartCellRenderer.java @@ -0,0 +1,49 @@ +/** + * <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.editor.overview; + +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiCellRenderer; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableComponent; +import org.olat.core.gui.render.Renderer; +import org.olat.core.gui.render.StringOutput; +import org.olat.core.gui.render.URLBuilder; +import org.olat.core.gui.translator.Translator; + +/** + * + * Initial date: 15 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class HierarchicalPartCellRenderer implements FlexiCellRenderer { + + @Override + public void render(Renderer renderer, StringOutput target, Object cellValue, int row, FlexiTableComponent source, + URLBuilder ubu, Translator translator) { + if(cellValue instanceof ControlObjectRow) { + ControlObjectRow controlObject = (ControlObjectRow)cellValue; + int depth = controlObject.getDepth(); + target.append("<div class='o_table_flexi_l").append(depth).append("'><span><i class='o_icon ").append(controlObject.getIconCssClass()).append("'> </i> ") + .append(controlObject.getTitle()).append("</span></div>"); + } else if(cellValue instanceof String) { + target.append((String)cellValue); + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxAttemptOption.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxAttemptOption.java new file mode 100644 index 0000000000000000000000000000000000000000..b047ef7210c06545aa467a2d58a592423a68095c --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxAttemptOption.java @@ -0,0 +1,50 @@ +/** + * <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.editor.overview; + +/** + * + * Initial date: 15 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class MaxAttemptOption { + + private final OptionEnum option; + private final Integer maxAttempts; + + private MaxAttemptOption(OptionEnum option, Integer maxAttempts) { + this.option = option; + this.maxAttempts = maxAttempts; + } + + public static MaxAttemptOption valueOf(OptionEnum option, Integer maxAttempts) { + return new MaxAttemptOption(option, maxAttempts); + } + + public OptionEnum getOption() { + return option; + } + + public Integer getMaxAttempts() { + return maxAttempts; + } + +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxAttemptsCellRenderer.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxAttemptsCellRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..f493aaeb508f8d398eb42da5def176834519850b --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxAttemptsCellRenderer.java @@ -0,0 +1,53 @@ +/** + * <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.editor.overview; + +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableComponent; +import org.olat.core.gui.render.Renderer; +import org.olat.core.gui.render.StringOutput; +import org.olat.core.gui.render.URLBuilder; +import org.olat.core.gui.translator.Translator; + +/** + * + * Initial date: 15 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class MaxAttemptsCellRenderer extends OptionCellRenderer { + + public MaxAttemptsCellRenderer(Translator translator) { + super(translator); + } + + @Override + public void render(Renderer renderer, StringOutput target, Object cellValue, int row, FlexiTableComponent source, + URLBuilder ubu, Translator trans) { + if(cellValue instanceof MaxAttemptOption) { + MaxAttemptOption attempts = (MaxAttemptOption)cellValue; + OptionEnum option = attempts.getOption(); + if(option == OptionEnum.yes) { + target.append(attempts.getMaxAttempts()); + } else { + super.render(renderer, target, option, row, source, ubu, trans); + } + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/OptionCellRenderer.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/OptionCellRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..63555d8dcde51f65dddf879c3221ee45f7c49b67 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/OptionCellRenderer.java @@ -0,0 +1,51 @@ +/** + * <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.editor.overview; + +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiCellRenderer; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableComponent; +import org.olat.core.gui.render.Renderer; +import org.olat.core.gui.render.StringOutput; +import org.olat.core.gui.render.URLBuilder; +import org.olat.core.gui.translator.Translator; + +/** + * + * Initial date: 15 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class OptionCellRenderer implements FlexiCellRenderer { + + private final Translator translator; + + public OptionCellRenderer(Translator translator) { + this.translator = translator; + } + + @Override + public void render(Renderer renderer, StringOutput target, Object cellValue, int row, FlexiTableComponent source, + URLBuilder ubu, Translator trans) { + if(cellValue instanceof OptionEnum) { + OptionEnum option = (OptionEnum)cellValue; + target.append(translator.translate("configuration.option.".concat(option.name()))); + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/OptionEnum.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/OptionEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..9464b5ab60aeb5430b33c7e84c764e24fef14109 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/OptionEnum.java @@ -0,0 +1,34 @@ +/** + * <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.editor.overview; + +/** + * + * Initial date: 15 janv. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public enum OptionEnum { + + yes, + no, + inherited + +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/_content/max_time_limit.html b/src/main/java/org/olat/ims/qti21/ui/editor/overview/_content/max_time_limit.html new file mode 100644 index 0000000000000000000000000000000000000000..58dcb822d4230bcf230b34f878c0c60dac508eb7 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/_content/max_time_limit.html @@ -0,0 +1 @@ +<div class="form-inline">$r.render("time.limit.hour") $r.translate("hour.short") $r.render("time.limit.minute") $r.translate("minute.short")</div> diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/_content/overview.html b/src/main/java/org/olat/ims/qti21/ui/editor/overview/_content/overview.html new file mode 100644 index 0000000000000000000000000000000000000000..52631ccca1ac114020072b8fc06c7110b08f9926 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/_content/overview.html @@ -0,0 +1,2 @@ +$r.render("infos") +$r.render("overview") \ No newline at end of file diff --git a/src/test/java/org/olat/core/commons/services/taskexecutor/TaskExecutorManagerTest.java b/src/test/java/org/olat/core/commons/services/taskexecutor/TaskExecutorManagerTest.java deleted file mode 100644 index 77c911846bc8889c854996105d134f3fef5715d8..0000000000000000000000000000000000000000 --- a/src/test/java/org/olat/core/commons/services/taskexecutor/TaskExecutorManagerTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/** - * <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.core.commons.services.taskexecutor; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.Assert; -import org.junit.Test; -import org.olat.core.commons.services.taskexecutor.manager.TaskExecutorManagerImpl; -import org.olat.test.OlatTestCase; -import org.springframework.beans.factory.annotation.Autowired; - -/** - * - * Initial date: 02.07.2013<br> - * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com - * - */ -public class TaskExecutorManagerTest extends OlatTestCase { - - @Autowired - private TaskExecutorManagerImpl taskExecutorManager; - - @Test - public void testRunTask() { - final CountDownLatch finishCount = new CountDownLatch(1); - taskExecutorManager.execute(new DummyTask(finishCount)); - - try { - boolean zero = finishCount.await(10, TimeUnit.SECONDS); - Assert.assertTrue(zero); - } catch (InterruptedException e) { - Assert.fail("Takes too long (more than 10sec)"); - } - } - - public static class DummyTask implements Runnable { - - private final CountDownLatch finishCount; - - public DummyTask(CountDownLatch finishCount) { - this.finishCount = finishCount; - } - - @Override - public void run() { - finishCount.countDown(); - } - } -} diff --git a/src/test/java/org/olat/core/commons/services/taskexecutor/PersistentTaskDAOTest.java b/src/test/java/org/olat/core/commons/services/taskexecutor/manager/PersistentTaskDAOTest.java similarity index 97% rename from src/test/java/org/olat/core/commons/services/taskexecutor/PersistentTaskDAOTest.java rename to src/test/java/org/olat/core/commons/services/taskexecutor/manager/PersistentTaskDAOTest.java index 2df6a03b6081c1af1700d12733b6db8fb3bc40a2..d7d2e2fea6972218736d8c1ffaa2a7ff3210682e 100644 --- a/src/test/java/org/olat/core/commons/services/taskexecutor/PersistentTaskDAOTest.java +++ b/src/test/java/org/olat/core/commons/services/taskexecutor/manager/PersistentTaskDAOTest.java @@ -17,7 +17,7 @@ * frentix GmbH, http://www.frentix.com * <p> */ -package org.olat.core.commons.services.taskexecutor; +package org.olat.core.commons.services.taskexecutor.manager; import java.io.Serializable; import java.util.Date; @@ -29,6 +29,8 @@ import javax.persistence.EntityNotFoundException; import org.junit.Assert; import org.junit.Test; import org.olat.core.commons.persistence.DB; +import org.olat.core.commons.services.taskexecutor.Task; +import org.olat.core.commons.services.taskexecutor.TaskStatus; import org.olat.core.commons.services.taskexecutor.manager.PersistentTaskDAO; import org.olat.core.commons.services.taskexecutor.model.PersistentTask; import org.olat.core.id.Identity; @@ -113,7 +115,7 @@ public class PersistentTaskDAOTest extends OlatTestCase { PersistentTask task = persistentTaskDao.createTask(taskName, new DummyTask()); dbInstance.commitAndCloseSession(); - PersistentTask todo = persistentTaskDao.pickTaskForRun(task.getKey()); + PersistentTask todo = persistentTaskDao.pickTaskForRun(task); Assert.assertNotNull(todo); Assert.assertEquals(task.getKey(), todo.getKey()); @@ -128,7 +130,7 @@ public class PersistentTaskDAOTest extends OlatTestCase { dbInstance.commitAndCloseSession(); //update - PersistentTask todo = persistentTaskDao.pickTaskForRun(task.getKey()); + PersistentTask todo = persistentTaskDao.pickTaskForRun(task); DummyTask taskToUpdate = new DummyTask(); taskToUpdate.setMarkerValue("new marker"); persistentTaskDao.updateTask(todo, taskToUpdate, null, null); @@ -165,7 +167,8 @@ public class PersistentTaskDAOTest extends OlatTestCase { int count = 0; List<Long> todos = persistentTaskDao.tasksToDo(); for(Long todo:todos) { - PersistentTask taskToDo = persistentTaskDao.pickTaskForRun(todo); + PersistentTask loadedTask = persistentTaskDao.loadTaskById(todo); + PersistentTask taskToDo = persistentTaskDao.pickTaskForRun(loadedTask); persistentTaskDao.taskDone(taskToDo); count++; } @@ -452,7 +455,8 @@ public class PersistentTaskDAOTest extends OlatTestCase { int count = 0; List<Long> todos = persistentTaskDao.tasksToDo(); for(Long todo:todos) { - PersistentTask taskToDo = persistentTaskDao.pickTaskForRun(todo); + PersistentTask loadedTask = persistentTaskDao.loadTaskById(todo); + PersistentTask taskToDo = persistentTaskDao.pickTaskForRun(loadedTask); persistentTaskDao.taskDone(taskToDo); count++; } diff --git a/src/test/java/org/olat/core/commons/services/taskexecutor/manager/TaskExecutorManagerTest.java b/src/test/java/org/olat/core/commons/services/taskexecutor/manager/TaskExecutorManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..63afc7f508be2f216a2b1d6624175bce771f4bdb --- /dev/null +++ b/src/test/java/org/olat/core/commons/services/taskexecutor/manager/TaskExecutorManagerTest.java @@ -0,0 +1,153 @@ +/** + * <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.core.commons.services.taskexecutor.manager; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Assert; +import org.junit.Test; +import org.olat.core.commons.persistence.DB; +import org.olat.core.commons.services.taskexecutor.LongRunnable; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; +import org.olat.test.OlatTestCase; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * + * Initial date: 02.07.2013<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class TaskExecutorManagerTest extends OlatTestCase { + + private static final OLog log = Tracing.createLoggerFor(TaskExecutorManagerTest.class); + + @Autowired + private DB dbInstance; + @Autowired + private PersistentTaskDAO persistentTaskDao; + @Autowired + private TaskExecutorManagerImpl taskExecutorManager; + + @Test + public void testRunTask() { + final CountDownLatch finishCount = new CountDownLatch(1); + taskExecutorManager.execute(new DummyTask(finishCount)); + + try { + boolean zero = finishCount.await(10, TimeUnit.SECONDS); + Assert.assertTrue(zero); + } catch (InterruptedException e) { + Assert.fail("Takes too long (more than 10sec)"); + } + } + + + private final static int numOfMpTasks = 100; + private final static Map<String,AtomicInteger> taskCounter = new ConcurrentHashMap<>(); + private final static CountDownLatch mpFinishCount = new CountDownLatch(numOfMpTasks); + + @Test + public void testRunParallelTasks() { + for(int i=numOfMpTasks; i-->0;) { + String taskName = "Task-" + i; + persistentTaskDao.createTask(taskName, new DummySerializableTask(taskName)); + taskCounter.put(taskName, new AtomicInteger(0)); + dbInstance.commitAndCloseSession(); + } + + final int numOfExecutors = 5; + final Thread[] executors = new Thread[numOfExecutors]; + for(int i=numOfExecutors; i-->0; ) { + executors[i] = new Thread(new ProcessTask(taskExecutorManager)); + } + + try { + for(int i=numOfExecutors; i-->0; ) { + executors[i].start(); + } + + boolean zero = mpFinishCount.await(120, TimeUnit.SECONDS); + Assert.assertTrue(zero); + + // make sure the task is done only once + for(AtomicInteger count:taskCounter.values()) { + Assert.assertEquals(1, count.get()); + } + log.info("All task done: " + taskCounter.size()); + } catch (InterruptedException e) { + Assert.fail("Takes too long (more than 10sec)"); + } + } + + public static class ProcessTask implements Runnable { + + private final TaskExecutorManagerImpl executor; + + public ProcessTask(TaskExecutorManagerImpl executor) { + this.executor = executor; + } + + @Override + public void run() { + executor.processTaskToDo(); + } + } + + public static class DummySerializableTask implements LongRunnable, Runnable { + + private static final long serialVersionUID = 7459138015999298102L; + + private final String taskName; + public DummySerializableTask(String taskName) { + this.taskName = taskName; + } + + @Override + public void run() { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // + } + taskCounter.get(taskName).incrementAndGet(); + mpFinishCount.countDown(); + } + } + + public static class DummyTask implements Runnable { + + private final CountDownLatch finishCount; + + public DummyTask(CountDownLatch finishCount) { + this.finishCount = finishCount; + } + + @Override + public void run() { + finishCount.countDown(); + } + } +} diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java index 4e963be9040df166ec33cc2261d5a5ab02e306be..eeff6ddc72480072ef4e703fdce3bcab091511c8 100644 --- a/src/test/java/org/olat/test/AllTestsJunit4.java +++ b/src/test/java/org/olat/test/AllTestsJunit4.java @@ -110,8 +110,8 @@ import org.junit.runners.Suite; org.olat.core.commons.services.webdav.manager.WebDAVAuthManagerTest.class, org.olat.core.commons.services.webdav.servlets.RequestUtilsTest.class, org.olat.core.commons.services.sms.manager.MessageLogDAOTest.class, - org.olat.core.commons.services.taskexecutor.PersistentTaskDAOTest.class, - org.olat.core.commons.services.taskexecutor.TaskExecutorManagerTest.class, + org.olat.core.commons.services.taskexecutor.manager.PersistentTaskDAOTest.class, + org.olat.core.commons.services.taskexecutor.manager.TaskExecutorManagerTest.class, org.olat.core.commons.services.text.TextServiceTest.class, org.olat.admin.user.delete.service.UserDeletionManagerTest.class, org.olat.group.BusinessGroupManagedFlagsTest.class,