From 946c9abf33709ca8ddfb63b0bf76949833e4d89a Mon Sep 17 00:00:00 2001 From: srosse <none@none> Date: Wed, 14 Dec 2011 10:35:02 +0100 Subject: [PATCH] FXOLAT-396: add last update/initial course launch date in assessment tools --- .../assessment/AssessmentMainController.java | 48 +++++++--- .../java/org/olat/core/util/ExportUtil.java | 3 +- .../ScoreAccountingArchiveController.java | 19 ++-- .../archiver/ScoreAccountingHelper.java | 87 ++++++++++++++----- .../course/archiver/_content/feedback.html | 1 + .../archiver/_i18n/LocalStrings_de.properties | 3 + .../archiver/_i18n/LocalStrings_en.properties | 3 + .../AssessedIdentitiesTableDataModel.java | 12 ++- .../assessment/AssessedIdentityWrapper.java | 23 ++++- .../course/assessment/AssessmentHelper.java | 45 +++++++--- .../assessment/AssessmentMainController.java | 47 +++++++--- .../course/assessment/AssessmentManager.java | 10 +++ .../IdentityAssessmentEditController.java | 5 +- .../NewCachePersistingAssessmentManager.java | 47 ++++++++-- .../_i18n/LocalStrings_de.properties | 2 + .../_i18n/LocalStrings_en.properties | 2 + .../properties/CoursePropertyManager.java | 11 +++ .../PersistingCoursePropertyManager.java | 6 ++ .../run/preview/PreviewAssessmentManager.java | 6 ++ .../preview/PreviewCoursePropertyManager.java | 6 ++ .../properties/NarrowedPropertyManager.java | 14 +++ .../org/olat/properties/PropertyManager.java | 38 ++++++++ 22 files changed, 364 insertions(+), 74 deletions(-) diff --git a/src/main/java/de/bps/course/assessment/AssessmentMainController.java b/src/main/java/de/bps/course/assessment/AssessmentMainController.java index c919dcf6f26..0b90f817b78 100644 --- a/src/main/java/de/bps/course/assessment/AssessmentMainController.java +++ b/src/main/java/de/bps/course/assessment/AssessmentMainController.java @@ -27,6 +27,8 @@ package de.bps.course.assessment; import java.rmi.RemoteException; import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -79,6 +81,7 @@ import org.olat.core.logging.OLATSecurityException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.logging.activity.ActionType; +import org.olat.core.util.StringHelper; import org.olat.core.util.Util; import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.resource.OresHelper; @@ -95,7 +98,6 @@ import org.olat.course.assessment.GroupAndContextTableModel; import org.olat.course.assessment.IAssessmentCallback; import org.olat.course.assessment.IdentityAssessmentEditController; import org.olat.course.assessment.IndentedNodeRenderer; -import de.bps.course.assessment.NodeTableDataModelOnyx; import org.olat.course.condition.Condition; import org.olat.course.condition.interpreter.ConditionExpression; import org.olat.course.condition.interpreter.OnlyGroupConditionInterpreter; @@ -104,10 +106,12 @@ import org.olat.course.nodes.AssessableCourseNode; import org.olat.course.nodes.CourseNode; import org.olat.course.nodes.CourseNodeFactory; import org.olat.course.nodes.STCourseNode; +import org.olat.course.properties.CoursePropertyManager; import org.olat.course.run.userview.UserCourseEnvironment; import org.olat.course.run.userview.UserCourseEnvironmentImpl; import org.olat.group.BusinessGroup; import org.olat.group.ui.context.BGContextTableModel; +import org.olat.properties.Property; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryManager; import org.olat.user.UserManager; @@ -169,7 +173,8 @@ public class AssessmentMainController extends MainLayoutBasicController implemen // Hash map to keep references to already created user course environments // Serves as a local cache to reduce database access - not shared by multiple threads - Map<Long, UserCourseEnvironment> localUserCourseEnvironmentCache; // package visibility for avoiding synthetic accessor method + final Map<Long, UserCourseEnvironment> localUserCourseEnvironmentCache; // package visibility for avoiding synthetic accessor method + final Map<Long, Date> initialLaunchDates; // List of groups to which the user has access rights in this course private List<BusinessGroup> coachedGroups; @@ -224,6 +229,7 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea this.ores = ores; this.callback = assessmentCallback; this.localUserCourseEnvironmentCache = new HashMap<Long, UserCourseEnvironment>(); + this.initialLaunchDates = new HashMap<Long, Date>(); //use the PropertyHandlerTranslator as tableCtr translator propertyHandlerTranslator = UserManager.getInstance().getPropertyHandlerTranslator(getTranslator()); @@ -330,7 +336,7 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea } // select user - this.assessedIdentityWrapper = AssessmentHelper.wrapIdentity(focusOnIdentity, this.localUserCourseEnvironmentCache, course, null); + assessedIdentityWrapper = AssessmentHelper.wrapIdentity(focusOnIdentity, localUserCourseEnvironmentCache, initialLaunchDates, course, null); UserCourseEnvironment chooseUserCourseEnv = assessedIdentityWrapper.getUserCourseEnvironment(); identityAssessmentController = new IdentityAssessmentEditController(getWindowControl(),ureq, chooseUserCourseEnv, course, true); @@ -510,8 +516,8 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea // in user MODE_USERFOCUS, a simple identity table is used, no wrapped identites UserTableDataModel userListModel = (UserTableDataModel) userListCtr.getTableDataModel(); Identity assessedIdentity = userListModel.getIdentityAt(rowid); - this.assessedIdentityWrapper = AssessmentHelper.wrapIdentity(assessedIdentity, - this.localUserCourseEnvironmentCache, course, null); + assessedIdentityWrapper = AssessmentHelper.wrapIdentity(assessedIdentity, + localUserCourseEnvironmentCache, initialLaunchDates, course, null); } else { // all other cases where user can be choosen the assessed identity wrapper is used AssessedIdentitiesTableDataModel userListModel = (AssessedIdentitiesTableDataModel) userListCtr.getTableDataModel(); @@ -578,8 +584,8 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea if (aiwList.contains(this.assessedIdentityWrapper)) { ICourse course = CourseFactory.loadCourse(ores); aiwList.remove(this.assessedIdentityWrapper); - this.assessedIdentityWrapper = AssessmentHelper.wrapIdentity(this.assessedIdentityWrapper.getIdentity(), - this.localUserCourseEnvironmentCache, course, currentCourseNode); + assessedIdentityWrapper = AssessmentHelper.wrapIdentity(assessedIdentityWrapper.getIdentity(), + localUserCourseEnvironmentCache, initialLaunchDates, course, currentCourseNode); aiwList.add(this.assessedIdentityWrapper); userListCtr.modelChanged(); } @@ -676,7 +682,14 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea // 2.2) update wrapper object if (wrappedIdFromModel != null) { wrappers.remove(wrappedIdFromModel); - wrappedIdFromModel = AssessmentHelper.wrapIdentity(wrappedIdFromModel.getUserCourseEnvironment(), currentCourseNode); + + Date initialLaunchDate; + if(initialLaunchDates.containsKey(identityKeyFromEvent)) { + initialLaunchDate = initialLaunchDates.get(identityKeyFromEvent); + } else { + initialLaunchDate = AssessmentHelper.getInitialLaunchDate(wrappedIdFromModel.getUserCourseEnvironment()); + } + wrappedIdFromModel = AssessmentHelper.wrapIdentity(wrappedIdFromModel.getUserCourseEnvironment(), initialLaunchDate, currentCourseNode); wrappers.add(wrappedIdFromModel); userListCtr.modelChanged(); } @@ -822,7 +835,7 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea Identity identity = (Identity) identities.get(i); // if course node is null the wrapper will only contain the identity and no score information AssessedIdentityWrapper aiw = AssessmentHelper.wrapIdentity(identity, - this.localUserCourseEnvironmentCache, course, courseNode); + localUserCourseEnvironmentCache, initialLaunchDates, course, courseNode); wrappedIdentities.add(aiw); } // Add the wrapped identities to the table data model @@ -1225,9 +1238,20 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea // 2) preload controller local user environment cache start = System.currentTimeMillis(); List<Identity> identities = getAllIdentitisFromGroupmanagement(); - for (Iterator<Identity> iter = identities.iterator(); iter.hasNext();) { - Identity identity = iter.next(); - AssessmentHelper.wrapIdentity(identity, localUserCourseEnvironmentCache, course, null); + + CourseNode node = course.getCourseEnvironment().getRunStructure().getRootNode(); + CoursePropertyManager pm = course.getCourseEnvironment().getCoursePropertyManager(); + List<Property> firstTime = pm.findCourseNodeProperties(node, identities, ICourse.PROPERTY_INITIAL_LAUNCH_DATE); + Calendar cal = Calendar.getInstance(); + for(Property property:firstTime) { + if (StringHelper.containsNonWhitespace(property.getStringValue()) && property.getIdentity() != null) { + cal.setTimeInMillis(Long.parseLong(property.getStringValue())); + initialLaunchDates.put(property.getIdentity().getKey(), cal.getTime()); + } + } + + for (Identity identity : identities) { + AssessmentHelper.wrapIdentity(identity, localUserCourseEnvironmentCache, initialLaunchDates, course, null); if (Thread.interrupted()) break; } if (logDebug) { diff --git a/src/main/java/org/olat/core/util/ExportUtil.java b/src/main/java/org/olat/core/util/ExportUtil.java index a1b356b62aa..6583716688e 100644 --- a/src/main/java/org/olat/core/util/ExportUtil.java +++ b/src/main/java/org/olat/core/util/ExportUtil.java @@ -52,10 +52,11 @@ public class ExportUtil { * @param exportDirectory */ - public static void writeContentToFile(String fileName, String content, File exportDirectory, String enc) { + public static File writeContentToFile(String fileName, String content, File exportDirectory, String enc) { File f = new File(exportDirectory, fileName); if (f.exists()) { throw new AssertException("File " + fileName + " already exists!"); } FileUtils.save(f, content, enc); + return f; } /** diff --git a/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java b/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java index 6d7b8448709..d0f98a0022a 100644 --- a/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java +++ b/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java @@ -37,8 +37,11 @@ import org.olat.core.gui.components.velocity.VelocityContainer; import org.olat.core.gui.control.DefaultController; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; +import org.olat.core.gui.media.FileMediaResource; +import org.olat.core.gui.media.MediaResource; import org.olat.core.gui.translator.PackageTranslator; import org.olat.core.gui.translator.Translator; +import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.util.ExportUtil; import org.olat.core.util.Util; @@ -56,14 +59,12 @@ public class ScoreAccountingArchiveController extends DefaultController { private static final String PACKAGE = Util.getPackageName(ScoreAccountingArchiveController.class); private static final String VELOCITY_ROOT = Util.getPackageVelocityRoot(PACKAGE); - private static final String CMD_START = "cmd.start"; - private OLATResourceable ores; private Panel myPanel; private VelocityContainer myContent; private VelocityContainer vcFeedback; private Translator t; - private Link startButton; + private Link startButton, downloadButton; /** * Constructor for the score accounting archive controller @@ -93,7 +94,7 @@ public class ScoreAccountingArchiveController extends DefaultController { public void event(UserRequest ureq, Component source, Event event) { if (source == startButton) { ICourse course = CourseFactory.loadCourse(ores); - List users = ScoreAccountingHelper.loadUsers(course.getCourseEnvironment()); + List<Identity> users = ScoreAccountingHelper.loadUsers(course.getCourseEnvironment()); List nodes = ScoreAccountingHelper.loadAssessableNodes(course.getCourseEnvironment()); String result = ScoreAccountingHelper.createCourseResultsOverviewTable(users, nodes, course, ureq.getLocale()); @@ -107,11 +108,19 @@ public class ScoreAccountingArchiveController extends DefaultController { UserManager um = UserManager.getInstance(); String charset = um.getUserCharset(ureq.getIdentity()); - ExportUtil.writeContentToFile(fileName, result, exportDirectory, charset); + File downloadFile = ExportUtil.writeContentToFile(fileName, result, exportDirectory, charset); vcFeedback = new VelocityContainer("feedback", VELOCITY_ROOT + "/feedback.html", t, this); vcFeedback.contextPut("body", vcFeedback.getTranslator().translate("course.res.feedback", new String[] { fileName })); + downloadButton = LinkFactory.createButtonSmall("cmd.download", vcFeedback, this); + downloadButton.setUserObject(downloadFile); myPanel.setContent(vcFeedback); + } else if(source == downloadButton) { + File file = (File)downloadButton.getUserObject(); + if(file != null) { + MediaResource resource = new FileMediaResource(file); + ureq.getDispatchResult().setResultingMediaResource(resource); + } } } diff --git a/src/main/java/org/olat/course/archiver/ScoreAccountingHelper.java b/src/main/java/org/olat/course/archiver/ScoreAccountingHelper.java index 0cff13895f6..7f8119b944d 100644 --- a/src/main/java/org/olat/course/archiver/ScoreAccountingHelper.java +++ b/src/main/java/org/olat/course/archiver/ScoreAccountingHelper.java @@ -26,18 +26,21 @@ package org.olat.course.archiver; import java.util.ArrayList; -import java.util.Iterator; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import org.olat.basesecurity.BaseSecurity; import org.olat.basesecurity.BaseSecurityManager; import org.olat.basesecurity.SecurityGroup; -import org.olat.core.gui.translator.PackageTranslator; import org.olat.core.gui.translator.Translator; import org.olat.core.id.Identity; import org.olat.core.id.IdentityEnvironment; import org.olat.core.id.OLATResourceable; +import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.core.util.Util; import org.olat.core.util.resource.OresHelper; @@ -49,11 +52,13 @@ import org.olat.course.groupsandrights.CourseGroupManager; import org.olat.course.nodes.AssessableCourseNode; import org.olat.course.nodes.CourseNode; import org.olat.course.nodes.STCourseNode; +import org.olat.course.properties.CoursePropertyManager; 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.BusinessGroup; +import org.olat.properties.Property; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryManager; import org.olat.user.UserManager; @@ -64,8 +69,7 @@ import org.olat.user.propertyhandlers.UserPropertyHandler; * Comment: Provides functionality to get a course results overview. */ public class ScoreAccountingHelper { - private static final String PACKAGE = Util.getPackageName(ScoreAccountingArchiveController.class); - + /** * The results from assessable nodes are written to one row per user into an excel-sheet. An * assessable node will only appear if it is producing at least one of the @@ -77,9 +81,9 @@ public class ScoreAccountingHelper { * @param locale * @return String */ - public static String createCourseResultsOverviewTable(List identities, List myNodes, ICourse course, Locale locale) { - Translator t = new PackageTranslator(PACKAGE, locale); - StringBuilder tableHeader1 = new StringBuilder(); + public static String createCourseResultsOverviewTable(List<Identity> identities, List<AssessableCourseNode> myNodes, ICourse course, Locale locale) { + Translator t = Util.createPackageTranslator(ScoreAccountingArchiveController.class, locale); + StringBuilder tableHeader1 = new StringBuilder(); StringBuilder tableHeader2 = new StringBuilder(); StringBuilder tableContent = new StringBuilder(); StringBuilder table = new StringBuilder(); @@ -92,6 +96,8 @@ public class ScoreAccountingHelper { String co = t.translate("column.header.comment"); String cco = t.translate("column.header.coachcomment"); String at = t.translate("column.header.attempts"); + String il = t.translate("column.header.initialLaunchDate"); + String slm = t.translate("column.header.scoreLastModified"); String na = t.translate("column.field.notavailable"); String mi = t.translate("column.field.missing"); String yes = t.translate("column.field.yes"); @@ -108,6 +114,10 @@ public class ScoreAccountingHelper { // get user property handlers for this export, translate using the fallback // translator configured in the property handler + //Initial launch date + tableHeader1.append(il).append("\t"); + tableHeader2.append("\t"); + List<UserPropertyHandler> userPropertyHandlers = UserManager.getInstance().getUserPropertyHandlersFor( ScoreAccountingHelper.class.getCanonicalName(), true); t = UserManager.getInstance().getPropertyHandlerTranslator(t); @@ -115,22 +125,44 @@ public class ScoreAccountingHelper { tableHeader1.append(t.translate(propertyHandler.i18nColumnDescriptorLabelKey())); tableHeader1.append("\t"); tableHeader2.append("\t"); - } + } // preload user properties cache - course.getCourseEnvironment().getAssessmentManager().preloadCache(); + CourseEnvironment courseEnvironment = course.getCourseEnvironment(); + AssessmentManager assessmentManager = courseEnvironment.getAssessmentManager(); + assessmentManager.preloadCache(); boolean firstIteration = true; int rowNumber = 1; - Iterator iterIdentities = identities.iterator(); - while (iterIdentities.hasNext()) { - Identity identity = (Identity) iterIdentities.next(); + + CourseNode node = courseEnvironment.getRunStructure().getRootNode(); + CoursePropertyManager pm = courseEnvironment.getCoursePropertyManager(); + List<Property> firstTime = pm.findCourseNodeProperties(node, identities, ICourse.PROPERTY_INITIAL_LAUNCH_DATE); + Map<Identity,Date> firstTimes = new HashMap<Identity,Date>((identities.size() * 2) + 1); + Calendar cal = Calendar.getInstance(); + for(Property property:firstTime) { + if (StringHelper.containsNonWhitespace(property.getStringValue())) { + cal.setTimeInMillis(Long.parseLong(property.getStringValue())); + firstTimes.put(property.getIdentity(), cal.getTime()); + } + } + + Formatter formatter = Formatter.getInstance(locale); + + for (Identity identity:identities) { String uname = identity.getName(); tableContent.append(rowNumber); tableContent.append("\t"); tableContent.append(uname); tableContent.append("\t"); + + String initialLaunchDate = ""; + if(firstTimes.containsKey(identity)) { + initialLaunchDate = formatter.formatDateAndTime(firstTimes.get(identity)); + } + tableContent.append(initialLaunchDate).append("\t"); + // add dynamic user properties for (UserPropertyHandler propertyHandler : userPropertyHandlers) { String value = propertyHandler.getUserProperty(identity.getUser(), t.getLocale()); @@ -145,9 +177,7 @@ public class ScoreAccountingHelper { uce.getScoreAccounting().evaluateAll(); AssessmentManager am = course.getCourseEnvironment().getAssessmentManager(); - Iterator iterNodes = myNodes.iterator(); - while (iterNodes.hasNext()) { - AssessableCourseNode acnode = (AssessableCourseNode) iterNodes.next(); + for (AssessableCourseNode acnode:myNodes) { boolean scoreOk = acnode.hasScoreConfigured(); boolean passedOk = acnode.hasPassedConfigured(); boolean attemptsOk = acnode.hasAttemptsConfigured(); @@ -217,6 +247,20 @@ public class ScoreAccountingHelper { tableContent.append("\t"); } + if (firstIteration) { + //last Modified + tableHeader2.append(slm); + tableHeader2.append("\t"); + } + + String scoreLastModified = ""; + Date lastModified = am.getScoreLastModifiedDate(acnode, identity); + if(lastModified != null) { + scoreLastModified = formatter.formatDateAndTime(lastModified); + } + tableContent.append(scoreLastModified); + tableContent.append("\t"); + if (commentOk) { // Comments for user String comment = am.getNodeComment(acnode, identity); @@ -282,9 +326,7 @@ public class ScoreAccountingHelper { //fxdiff VCRP-4: assessment overview with max score StringBuilder tableFooter = new StringBuilder(); tableFooter.append("\t\n").append("\t\n").append(t.translate("legend")).append("\t\n").append("\t\n"); - Iterator iterNodes = myNodes.iterator(); - while (iterNodes.hasNext()) { - AssessableCourseNode acnode = (AssessableCourseNode) iterNodes.next(); + for (AssessableCourseNode acnode:myNodes) { if (!acnode.hasScoreConfigured()) { // only show min/max/cut legend when score configured continue; @@ -371,11 +413,10 @@ public class ScoreAccountingHelper { * @param courseEnv * @return The list of assessable nodes from this course */ - public static List loadAssessableNodes(CourseEnvironment courseEnv) { + public static List<AssessableCourseNode> loadAssessableNodes(CourseEnvironment courseEnv) { CourseNode rootNode = courseEnv.getRunStructure().getRootNode(); - List nodeList = new ArrayList(); + List<AssessableCourseNode> nodeList = new ArrayList<AssessableCourseNode>(); collectAssessableCourseNodes(rootNode, nodeList); - return nodeList; } @@ -385,9 +426,9 @@ public class ScoreAccountingHelper { * @param node * @param nodeList */ - private static void collectAssessableCourseNodes(CourseNode node, List nodeList) { + private static void collectAssessableCourseNodes(CourseNode node, List<AssessableCourseNode> nodeList) { if (node instanceof AssessableCourseNode) { - nodeList.add(node); + nodeList.add((AssessableCourseNode)node); } int count = node.getChildCount(); for (int i = 0; i < count; i++) { diff --git a/src/main/java/org/olat/course/archiver/_content/feedback.html b/src/main/java/org/olat/course/archiver/_content/feedback.html index 343baeadd3d..b18e2be1519 100644 --- a/src/main/java/org/olat/course/archiver/_content/feedback.html +++ b/src/main/java/org/olat/course/archiver/_content/feedback.html @@ -2,5 +2,6 @@ <p> $body </p> +<p>$r.render("cmd.download")</p> diff --git a/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_de.properties index 5dcc34c8957..1ae1e1079ff 100644 --- a/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_de.properties @@ -47,6 +47,7 @@ chelp.title=Titel chelp.uid=Benutzeridentifikation chelp.ziel=Ziel cmd.start=Start +cmd.download=Herunterladen column.field.missing=- column.field.no=nein column.field.notavailable=k. A. @@ -58,6 +59,8 @@ column.header.login=Benutzername column.header.passed=Bestanden column.header.score=Punkte column.header.seqnum=Nr. +column.header.initialLaunchDate=Erster Kursstart +column.header.scoreLastModified=Letzte Aktualisierung command.change.charset=Zeichensatz command.closearchiver=Schliessen course.logs.error=Sie haben kein Logfile ausgew\u00E4hlt. diff --git a/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_en.properties index 95dbbe1e925..42b77c689cc 100644 --- a/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_en.properties @@ -47,6 +47,7 @@ chelp.title=Title chelp.uid=User identification chelp.ziel=Target cmd.start=Start +cmd.download=Download column.field.missing=- column.field.no=No column.field.notavailable=n/a @@ -58,6 +59,8 @@ column.header.login=User name column.header.passed=Passed column.header.score=Score column.header.seqnum=No. +column.header.initialLaunchDate=Initial course launch +column.header.scoreLastModified=Last update command.change.charset=Character set command.closearchiver=Close course.logs.error=You did not select any log files. diff --git a/src/main/java/org/olat/course/assessment/AssessedIdentitiesTableDataModel.java b/src/main/java/org/olat/course/assessment/AssessedIdentitiesTableDataModel.java index d8e46bd05a0..ccebd9556e3 100644 --- a/src/main/java/org/olat/course/assessment/AssessedIdentitiesTableDataModel.java +++ b/src/main/java/org/olat/course/assessment/AssessedIdentitiesTableDataModel.java @@ -68,6 +68,8 @@ public class AssessedIdentitiesTableDataModel extends DefaultTableDataModel { private static final String COL_MAXSCORE = "maxScore"; private static final String COL_PASSED = "passed"; private static final String COL_STATUS = "status"; + private static final String COL_INITIAL_LAUNCH = "initialLaunch"; + private static final String COL_LAST_SCORE_DATE = "lastScoreDate"; private List<String> colMapping; private List<String> userPropertyNameList; @@ -127,6 +129,8 @@ public class AssessedIdentitiesTableDataModel extends DefaultTableDataModel { if (courseNode.hasPassedConfigured()) { colMapping.add(colCount++, COL_PASSED); } + colMapping.add(colCount++, COL_INITIAL_LAUNCH); + colMapping.add(colCount++, COL_LAST_SCORE_DATE); } } @@ -187,10 +191,14 @@ public class AssessedIdentitiesTableDataModel extends DefaultTableDataModel { return ""; } else if (colName.equals(COL_STATUS)) { return getStatusFor(courseNode, wrappedIdentity); - }else if (colName.equals(COL_PASSED)) { + } else if (colName.equals(COL_PASSED)) { ScoreEvaluation scoreEval = wrappedIdentity.getUserCourseEnvironment().getScoreAccounting().evalCourseNode(courseNode); if (scoreEval == null) scoreEval = new ScoreEvaluation(null, null); return scoreEval.getPassed(); + } else if (colName.equals(COL_INITIAL_LAUNCH)) { + return wrappedIdentity.getInitialLaunchDate(); + } else if (colName.equals(COL_LAST_SCORE_DATE)) { + return wrappedIdentity.getLastModified(); } else return "error"; } @@ -262,6 +270,8 @@ public class AssessedIdentitiesTableDataModel extends DefaultTableDataModel { userListCtr.addColumnDescriptor(new BooleanColumnDescriptor("table.header.passed", colCount++, translator.translate("passed.true"), translator.translate("passed.false"))); } + userListCtr.addColumnDescriptor(new DefaultColumnDescriptor("table.header.initialLaunchDate", colCount++, null, getLocale(), ColumnDescriptor.ALIGNMENT_LEFT)); + userListCtr.addColumnDescriptor(new DefaultColumnDescriptor("table.header.lastScoreDate", colCount++, null, getLocale(), ColumnDescriptor.ALIGNMENT_LEFT)); } } diff --git a/src/main/java/org/olat/course/assessment/AssessedIdentityWrapper.java b/src/main/java/org/olat/course/assessment/AssessedIdentityWrapper.java index 2f5a86e029d..ac967307b4d 100644 --- a/src/main/java/org/olat/course/assessment/AssessedIdentityWrapper.java +++ b/src/main/java/org/olat/course/assessment/AssessedIdentityWrapper.java @@ -25,6 +25,8 @@ package org.olat.course.assessment; +import java.util.Date; + import org.olat.core.id.Identity; import org.olat.course.run.userview.UserCourseEnvironment; @@ -37,9 +39,11 @@ import org.olat.course.run.userview.UserCourseEnvironment; * variables that should be displayed in the user list table. */ public class AssessedIdentityWrapper { - private UserCourseEnvironment userCourseEnvironment; - private Integer nodeAttempts; - private String detailsListView; + private final UserCourseEnvironment userCourseEnvironment; + private final Integer nodeAttempts; + private final String detailsListView; + private final Date initialLaunchDate; + private final Date lastModified; /** * Constructor for this identity wrapper object. Wraps an identity with @@ -48,11 +52,14 @@ public class AssessedIdentityWrapper { * @param nodeAttempts the users node attempts for the current node * @param detailsListView the users details for this node */ - public AssessedIdentityWrapper(UserCourseEnvironment userCourseEnvironment, Integer nodeAttempts, String detailsListView) { + public AssessedIdentityWrapper(UserCourseEnvironment userCourseEnvironment, Integer nodeAttempts, String detailsListView, + Date initialLaunchDate, Date lastModified) { super(); this.userCourseEnvironment = userCourseEnvironment; this.nodeAttempts = nodeAttempts; this.detailsListView = detailsListView; + this.initialLaunchDate = initialLaunchDate; + this.lastModified = lastModified; } /** @@ -83,4 +90,12 @@ public class AssessedIdentityWrapper { public Integer getNodeAttempts() { return nodeAttempts; } + + public Date getInitialLaunchDate() { + return initialLaunchDate; + } + + public Date getLastModified() { + return lastModified; + } } diff --git a/src/main/java/org/olat/course/assessment/AssessmentHelper.java b/src/main/java/org/olat/course/assessment/AssessmentHelper.java index 64fb18b9e2f..499d3921e64 100644 --- a/src/main/java/org/olat/course/assessment/AssessmentHelper.java +++ b/src/main/java/org/olat/course/assessment/AssessmentHelper.java @@ -28,6 +28,7 @@ package org.olat.course.assessment; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -36,7 +37,9 @@ import java.util.Map; import org.olat.core.id.Identity; import org.olat.core.id.IdentityEnvironment; +import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; +import org.olat.core.util.StringHelper; import org.olat.core.util.nodes.INode; import org.olat.core.util.tree.TreeVisitor; import org.olat.core.util.tree.Visitor; @@ -47,12 +50,14 @@ import org.olat.course.nodes.ProjectBrokerCourseNode; import org.olat.course.nodes.STCourseNode; import org.olat.course.nodes.ScormCourseNode; import org.olat.course.nodes.iq.IQEditController; +import org.olat.course.properties.CoursePropertyManager; import org.olat.course.run.scoring.ScoreEvaluation; import org.olat.course.run.userview.UserCourseEnvironment; import org.olat.course.run.userview.UserCourseEnvironmentImpl; import org.olat.course.tree.CourseEditorTreeModel; import org.olat.course.tree.CourseEditorTreeNode; import org.olat.modules.ModuleConfiguration; +import org.olat.properties.Property; /** * Description:<br> @@ -62,6 +67,8 @@ import org.olat.modules.ModuleConfiguration; * @author gnaegi */ public class AssessmentHelper { + + private static final OLog log = Tracing.createLoggerFor(AssessmentHelper.class); /** * String to symbolize 'not available' or 'not assigned' in assessments @@ -87,25 +94,25 @@ public class AssessmentHelper { * attempts must be fetched * @return a wrapped identity */ - public static AssessedIdentityWrapper wrapIdentity(Identity identity, Map<Long,UserCourseEnvironment> localUserCourseEnvironmentCache, ICourse course, - AssessableCourseNode courseNode) { + public static AssessedIdentityWrapper wrapIdentity(Identity identity, Map<Long,UserCourseEnvironment> localUserCourseEnvironmentCache, + Map<Long, Date> initialLaunchDates, ICourse course, AssessableCourseNode courseNode) { // Try to get user course environment from local hash map cache. If not // successful // create the environment and add it to the map for later performance // optimization - //synchronized (localUserCourseEnvironmentCache) { //o_clusterOK by:ld - no need to synchronized - only local variables UserCourseEnvironment uce = localUserCourseEnvironmentCache.get(identity.getKey()); if (uce == null) { uce = createAndInitUserCourseEnvironment(identity, course); // add to cache for later usage localUserCourseEnvironmentCache.put(identity.getKey(), uce); - if (Tracing.isDebugEnabled(AssessmentHelper.class)){ - Tracing.logDebug("localUserCourseEnvironmentCache hit failed, adding course environment for user::" - + identity.getName(), AssessmentHelper.class); + if (log.isDebug()){ + log.debug("localUserCourseEnvironmentCache hit failed, adding course environment for user::" + + identity.getName()); } } - return wrapIdentity(uce, courseNode); - //} + + Date initialLaunchDate = initialLaunchDates.get(identity.getKey()); + return wrapIdentity(uce, initialLaunchDate, courseNode); } /** @@ -118,7 +125,7 @@ public class AssessmentHelper { * attempts must be fetched * @return a wrapped identity */ - public static AssessedIdentityWrapper wrapIdentity(UserCourseEnvironment uce, AssessableCourseNode courseNode) { + public static AssessedIdentityWrapper wrapIdentity(UserCourseEnvironment uce, Date initialLaunchDate, AssessableCourseNode courseNode) { // Fetch attempts and details for this node if available Integer attempts = null; String details = null; @@ -131,9 +138,27 @@ public class AssessmentHelper { if (details == null) details = DETAILS_NA_VALUE; } } - AssessedIdentityWrapper aiw = new AssessedIdentityWrapper(uce, attempts, details); + + Identity identity = uce.getIdentityEnvironment().getIdentity(); + Date lastModified = uce.getCourseEnvironment().getAssessmentManager().getScoreLastModifiedDate(courseNode, identity); + AssessedIdentityWrapper aiw = new AssessedIdentityWrapper(uce, attempts, details, initialLaunchDate, lastModified); return aiw; } + + public static Date getInitialLaunchDate(UserCourseEnvironment uce) { + CourseNode node = uce.getCourseEnvironment().getRunStructure().getRootNode(); + CoursePropertyManager pm = uce.getCourseEnvironment().getCoursePropertyManager(); + Identity identity = uce.getIdentityEnvironment().getIdentity(); + Property firstTime = pm.findCourseNodeProperty(node, identity, null, ICourse.PROPERTY_INITIAL_LAUNCH_DATE); + + Date initialLaunchDate = null; + if (firstTime != null && StringHelper.containsNonWhitespace(firstTime.getStringValue())) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(Long.parseLong(firstTime.getStringValue())); + initialLaunchDate = cal.getTime(); + } + return initialLaunchDate; + } /** * Create a user course environment for the given user and course. After diff --git a/src/main/java/org/olat/course/assessment/AssessmentMainController.java b/src/main/java/org/olat/course/assessment/AssessmentMainController.java index 4b799adcae0..ac7d812267d 100644 --- a/src/main/java/org/olat/course/assessment/AssessmentMainController.java +++ b/src/main/java/org/olat/course/assessment/AssessmentMainController.java @@ -26,6 +26,8 @@ package org.olat.course.assessment; import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -78,6 +80,7 @@ import org.olat.core.logging.OLATSecurityException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.logging.activity.ActionType; +import org.olat.core.util.StringHelper; import org.olat.core.util.Util; import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.resource.OresHelper; @@ -92,10 +95,12 @@ import org.olat.course.nodes.CourseNode; import org.olat.course.nodes.CourseNodeFactory; import org.olat.course.nodes.ProjectBrokerCourseNode; import org.olat.course.nodes.STCourseNode; +import org.olat.course.properties.CoursePropertyManager; import org.olat.course.run.userview.UserCourseEnvironment; import org.olat.course.run.userview.UserCourseEnvironmentImpl; import org.olat.group.BusinessGroup; import org.olat.group.ui.context.BGContextTableModel; +import org.olat.properties.Property; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryManager; import org.olat.user.UserManager; @@ -146,7 +151,8 @@ public class AssessmentMainController extends MainLayoutBasicController implemen // Hash map to keep references to already created user course environments // Serves as a local cache to reduce database access - not shared by multiple threads - Map<Long, UserCourseEnvironment> localUserCourseEnvironmentCache; // package visibility for avoiding synthetic accessor method + final Map<Long, UserCourseEnvironment> localUserCourseEnvironmentCache; // package visibility for avoiding synthetic accessor method + final Map<Long, Date> initialLaunchDates; // List of groups to which the user has access rights in this course private List<BusinessGroup> coachedGroups; //Is tutor from the security group of repository entry @@ -188,7 +194,8 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea getUserActivityLogger().setStickyActionType(ActionType.admin); this.ores = ores; this.callback = assessmentCallback; - this.localUserCourseEnvironmentCache = new HashMap<Long, UserCourseEnvironment>(); + localUserCourseEnvironmentCache = new HashMap<Long, UserCourseEnvironment>(); + initialLaunchDates = new HashMap<Long,Date>(); //use the PropertyHandlerTranslator as tableCtr translator propertyHandlerTranslator = UserManager.getInstance().getPropertyHandlerTranslator(getTranslator()); @@ -290,7 +297,7 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea } // select user - this.assessedIdentityWrapper = AssessmentHelper.wrapIdentity(focusOnIdentity, this.localUserCourseEnvironmentCache, course, null); + assessedIdentityWrapper = AssessmentHelper.wrapIdentity(focusOnIdentity, localUserCourseEnvironmentCache, initialLaunchDates, course, null); UserCourseEnvironment chooseUserCourseEnv = assessedIdentityWrapper.getUserCourseEnvironment(); identityAssessmentController = new IdentityAssessmentEditController(getWindowControl(),ureq, chooseUserCourseEnv, course, true); @@ -419,8 +426,8 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea // in user MODE_USERFOCUS, a simple identity table is used, no wrapped identites UserTableDataModel userListModel = (UserTableDataModel) userListCtr.getTableDataModel(); Identity assessedIdentity = userListModel.getIdentityAt(rowid); - this.assessedIdentityWrapper = AssessmentHelper.wrapIdentity(assessedIdentity, - this.localUserCourseEnvironmentCache, course, null); + assessedIdentityWrapper = AssessmentHelper.wrapIdentity(assessedIdentity, + localUserCourseEnvironmentCache, initialLaunchDates, course, null); } else { // all other cases where user can be choosen the assessed identity wrapper is used AssessedIdentitiesTableDataModel userListModel = (AssessedIdentitiesTableDataModel) userListCtr.getTableDataModel(); @@ -480,7 +487,7 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea ICourse course = CourseFactory.loadCourse(ores); aiwList.remove(this.assessedIdentityWrapper); this.assessedIdentityWrapper = AssessmentHelper.wrapIdentity(this.assessedIdentityWrapper.getIdentity(), - this.localUserCourseEnvironmentCache, course, currentCourseNode); + this.localUserCourseEnvironmentCache, initialLaunchDates, course, currentCourseNode); aiwList.add(this.assessedIdentityWrapper); userListCtr.modelChanged(); } @@ -563,7 +570,14 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea // 2.2) update wrapper object if (wrappedIdFromModel != null) { wrappers.remove(wrappedIdFromModel); - wrappedIdFromModel = AssessmentHelper.wrapIdentity(wrappedIdFromModel.getUserCourseEnvironment(), currentCourseNode); + + Date initialLaunchDate; + if(initialLaunchDates.containsKey(identityKeyFromEvent)) { + initialLaunchDate = initialLaunchDates.get(identityKeyFromEvent); + } else { + initialLaunchDate = AssessmentHelper.getInitialLaunchDate(wrappedIdFromModel.getUserCourseEnvironment()); + } + wrappedIdFromModel = AssessmentHelper.wrapIdentity(wrappedIdFromModel.getUserCourseEnvironment(), initialLaunchDate, currentCourseNode); wrappers.add(wrappedIdFromModel); userListCtr.modelChanged(); } @@ -750,7 +764,7 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea Identity identity = identities.get(i); // if course node is null the wrapper will only contain the identity and no score information AssessedIdentityWrapper aiw = AssessmentHelper.wrapIdentity(identity, - this.localUserCourseEnvironmentCache, course, courseNode); + localUserCourseEnvironmentCache, initialLaunchDates, course, courseNode); wrappedIdentities.add(aiw); } // Add the wrapped identities to the table data model @@ -1161,9 +1175,20 @@ AssessmentMainController(UserRequest ureq, WindowControl wControl, OLATResourcea // 2) preload controller local user environment cache start = System.currentTimeMillis(); List<Identity> identities = getAllIdentitisFromGroupmanagement(); - for (Iterator<Identity> iter = identities.iterator(); iter.hasNext();) { - Identity identity = iter.next(); - AssessmentHelper.wrapIdentity(identity, localUserCourseEnvironmentCache, course, null); + + CourseNode node = course.getCourseEnvironment().getRunStructure().getRootNode(); + CoursePropertyManager pm = course.getCourseEnvironment().getCoursePropertyManager(); + List<Property> firstTime = pm.findCourseNodeProperties(node, identities, ICourse.PROPERTY_INITIAL_LAUNCH_DATE); + Calendar cal = Calendar.getInstance(); + for(Property property:firstTime) { + if (StringHelper.containsNonWhitespace(property.getStringValue()) && property.getIdentity() != null) { + cal.setTimeInMillis(Long.parseLong(property.getStringValue())); + initialLaunchDates.put(property.getIdentity().getKey(), cal.getTime()); + } + } + + for (Identity identity : identities) { + AssessmentHelper.wrapIdentity(identity, localUserCourseEnvironmentCache, initialLaunchDates, course, null); if (Thread.interrupted()) break; } if (logDebug) { diff --git a/src/main/java/org/olat/course/assessment/AssessmentManager.java b/src/main/java/org/olat/course/assessment/AssessmentManager.java index 95c7db85fb0..4f422eb77cb 100644 --- a/src/main/java/org/olat/course/assessment/AssessmentManager.java +++ b/src/main/java/org/olat/course/assessment/AssessmentManager.java @@ -25,6 +25,8 @@ package org.olat.course.assessment; +import java.util.Date; + import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.util.event.GenericEventListener; @@ -170,6 +172,14 @@ public interface AssessmentManager { */ public Long getAssessmentID(CourseNode courseNode, Identity identity); + /** + * + * @param courseNode + * @param identity + * @return + */ + public Date getScoreLastModifiedDate(CourseNode courseNode, Identity identity); + /** * Save the users achieved ScoreEvaluation for this node. If there is already a score property available, it will be * overwritten with the new value <p> diff --git a/src/main/java/org/olat/course/assessment/IdentityAssessmentEditController.java b/src/main/java/org/olat/course/assessment/IdentityAssessmentEditController.java index 4afb5ae1343..f7be1e5e1a9 100644 --- a/src/main/java/org/olat/course/assessment/IdentityAssessmentEditController.java +++ b/src/main/java/org/olat/course/assessment/IdentityAssessmentEditController.java @@ -25,6 +25,8 @@ package org.olat.course.assessment; +import java.util.Date; + import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.link.Link; @@ -156,7 +158,8 @@ public class IdentityAssessmentEditController extends BasicController { private void doEditNodeAssessment(UserRequest ureq, AssessableCourseNode courseNode){ if (mayEdit) { ICourse course = CourseFactory.loadCourse(ores); - AssessedIdentityWrapper assessedIdentityWrapper = AssessmentHelper.wrapIdentity(assessedUserCourseEnvironment, courseNode); + Date initialLaunchDate = AssessmentHelper.getInitialLaunchDate(assessedUserCourseEnvironment); + AssessedIdentityWrapper assessedIdentityWrapper = AssessmentHelper.wrapIdentity(assessedUserCourseEnvironment, initialLaunchDate, courseNode); assessmentEditCtr = new AssessmentEditController(ureq, getWindowControl(), course, courseNode, assessedIdentityWrapper); listenTo(assessmentEditCtr); main.setContent(assessmentEditCtr.getInitialComponent()); diff --git a/src/main/java/org/olat/course/assessment/NewCachePersistingAssessmentManager.java b/src/main/java/org/olat/course/assessment/NewCachePersistingAssessmentManager.java index 02f672b9f85..fefb470d7dd 100644 --- a/src/main/java/org/olat/course/assessment/NewCachePersistingAssessmentManager.java +++ b/src/main/java/org/olat/course/assessment/NewCachePersistingAssessmentManager.java @@ -26,8 +26,9 @@ package org.olat.course.assessment; import java.io.Serializable; +import java.util.Calendar; +import java.util.Date; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; @@ -107,6 +108,8 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements * (otherwise we cannot know whether a null value means expiration of cache or no-such-property-yet-for-user) */ private static final String FULLUSERSET = "FULLUSERSET"; + private static final String LAST_MODIFIED = "LAST_MODIFIED"; + // Float and Integer are immutable objects, we can reuse them. private static final Float FLOAT_ZERO = new Float(0); @@ -148,7 +151,7 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements * are loaded. * @return */ - private List loadPropertiesFor(Identity identity) { + private List<Property> loadPropertiesFor(Identity identity) { ICourse course = CourseFactory.loadCourse(ores); StringBuilder sb = new StringBuilder(); sb.append("from org.olat.properties.Property as p"); @@ -171,7 +174,7 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements if (identity != null) { query.setEntity("id", identity); } - List properties = query.list(); + List<Property> properties = query.list(); return properties; } @@ -211,9 +214,8 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements // or has been invalidated (in cluster mode when puts occurred from an other node for the same cache) m = new HashMap<String, Serializable>(); // load data - List properties = loadPropertiesFor(identity); - for (Iterator iter = properties.iterator(); iter.hasNext();) { - Property property = (Property) iter.next(); + List<Property> properties = loadPropertiesFor(identity); + for (Property property:properties) { addPropertyToCache(m, property); } // we use a putSilent here (no invalidation notifications to other cluster nodes), since @@ -288,10 +290,17 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements throw new AssertionError("property in list that is not of type attempts, score, passed or ASSESSMENT_ID, COMMENT and COACH_COMMENT :: " + propertyName); } + Date lastModified = property.getLastModified(); // put in cache, maybe overriding old values String cacheKey = getPropertyCacheKey(property); synchronized(acache) {//cluster_ok acache is an element from the cacher acache.put(cacheKey, value); + + String lmCacheKey = getLastModifiedCacheKey(property); + Long currentLastModifiedDate = (Long)acache.get(lmCacheKey); + if(currentLastModifiedDate == null || currentLastModifiedDate.longValue() < lastModified.getTime()) { + acache.put(lmCacheKey, new Long(lastModified.getTime())); + } } } @@ -689,6 +698,13 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements return cacheKey; } + private String getLastModifiedCacheKey(Property property) {; + String propertyCategory = property.getCategory(); + String nodeIdent = propertyCategory.substring(propertyCategory.indexOf("::") + 2); + String cacheKey = getCacheKey(nodeIdent, LAST_MODIFIED); + return cacheKey; + } + /** * @see org.olat.course.assessment.AssessmentManager#registerForAssessmentChangeEvents(org.olat.core.util.event.GenericEventListener, * org.olat.core.id.Identity) @@ -759,6 +775,25 @@ public class NewCachePersistingAssessmentManager extends BasicManager implements } } + @Override + public Date getScoreLastModifiedDate(CourseNode courseNode, Identity identity) { + if (courseNode == null) { + return null; // return default value + } + + String cacheKey = getCacheKey(courseNode, LAST_MODIFIED); + Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, false); + synchronized(m) {//o_clusterOK by:fj is per vm only + Long lastModified = (Long) m.get(cacheKey); + if(lastModified != null) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(lastModified.longValue()); + return cal.getTime(); + } + } + return null; + } + /** * * @see org.olat.course.assessment.AssessmentManager#saveScoreEvaluation(org.olat.course.nodes.CourseNode, org.olat.core.id.Identity, org.olat.core.id.Identity, org.olat.course.run.scoring.ScoreEvaluation) diff --git a/src/main/java/org/olat/course/assessment/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/assessment/_i18n/LocalStrings_de.properties index 53a906bd837..14c06179a80 100644 --- a/src/main/java/org/olat/course/assessment/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/course/assessment/_i18n/LocalStrings_de.properties @@ -141,6 +141,8 @@ table.header.score=Punkte table.header.show=Leistungsnachweis table.header.status=Status table.header.type=Typ +table.header.initialLaunchDate=Erster Kursstart +table.header.lastScoreDate=Letzte Aktualisierung notifications.title=Kurs "{0}" notifications.header=Neue Testresultate in Kurs "{0}" notifications.entry={3}: "{0}" ausgef\u00FChrt von {1} mit {2} Punkten diff --git a/src/main/java/org/olat/course/assessment/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/assessment/_i18n/LocalStrings_en.properties index 787b552c0d1..68f1d52ec81 100644 --- a/src/main/java/org/olat/course/assessment/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/course/assessment/_i18n/LocalStrings_en.properties @@ -149,6 +149,8 @@ table.header.score=Score table.header.show=Evidence of achievement table.header.status=Status table.header.type=Type +table.header.initialLaunchDate=Initial course launch +table.header.lastScoreDate=Last update title.infocoach=Information on assessment tool.name=Assessment tool userchoose.nousers=No users found. Either there are no persons assigned to those groups or you do not have any coaching rights. diff --git a/src/main/java/org/olat/course/properties/CoursePropertyManager.java b/src/main/java/org/olat/course/properties/CoursePropertyManager.java index 32a50cef94a..91ac73aa911 100644 --- a/src/main/java/org/olat/course/properties/CoursePropertyManager.java +++ b/src/main/java/org/olat/course/properties/CoursePropertyManager.java @@ -103,6 +103,17 @@ public interface CoursePropertyManager extends IdentityAnonymizerCallback { */ public Property findCourseNodeProperty(CourseNode node, Identity identity, BusinessGroup grp, String name); + + /** + * Find a specific course node property (exact match. I.e. null values are taken into account) + * @param node + * @param identity + * @param grp + * @param name + * @return matching course node property + */ + public List<Property> findCourseNodeProperties(CourseNode node, List<Identity> identities, String name); + /** * Delete all node properties for a given course node and a category. * @param courseNode The course node. Must not be null. diff --git a/src/main/java/org/olat/course/properties/PersistingCoursePropertyManager.java b/src/main/java/org/olat/course/properties/PersistingCoursePropertyManager.java index c3bd1d2e99c..d2cda015cc9 100644 --- a/src/main/java/org/olat/course/properties/PersistingCoursePropertyManager.java +++ b/src/main/java/org/olat/course/properties/PersistingCoursePropertyManager.java @@ -142,6 +142,12 @@ public class PersistingCoursePropertyManager extends BasicManager implements Cou return pm.findProperty(identity, grp, myCategory, name); } + @Override + public List<Property> findCourseNodeProperties(CourseNode node, List<Identity> identities, String name) { + String myCategory = buildCourseNodePropertyCategory(node); + return pm.findProperties(identities, myCategory, name); + } + /** * @see org.olat.course.properties.CoursePropertyManager#deleteNodeProperties(org.olat.course.nodes.CourseNode, * java.lang.String) diff --git a/src/main/java/org/olat/course/run/preview/PreviewAssessmentManager.java b/src/main/java/org/olat/course/run/preview/PreviewAssessmentManager.java index fdedee22984..481509411c1 100644 --- a/src/main/java/org/olat/course/run/preview/PreviewAssessmentManager.java +++ b/src/main/java/org/olat/course/run/preview/PreviewAssessmentManager.java @@ -25,6 +25,7 @@ package org.olat.course.run.preview; +import java.util.Date; import java.util.HashMap; import org.olat.core.id.Identity; @@ -185,6 +186,11 @@ final class PreviewAssessmentManager extends BasicManager implements AssessmentM return (Long)nodeAssessmentID.get(courseNode.getIdent()); } + @Override + public Date getScoreLastModifiedDate(CourseNode courseNode, Identity identity) { + return null; + } + /** * * @see org.olat.course.assessment.AssessmentManager#saveScoreEvaluation(org.olat.course.nodes.CourseNode, org.olat.core.id.Identity, org.olat.core.id.Identity, org.olat.course.run.scoring.ScoreEvaluation) diff --git a/src/main/java/org/olat/course/run/preview/PreviewCoursePropertyManager.java b/src/main/java/org/olat/course/run/preview/PreviewCoursePropertyManager.java index 1031fc67b09..1f5a7ef4ec0 100644 --- a/src/main/java/org/olat/course/run/preview/PreviewCoursePropertyManager.java +++ b/src/main/java/org/olat/course/run/preview/PreviewCoursePropertyManager.java @@ -26,6 +26,7 @@ package org.olat.course.run.preview; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -134,6 +135,11 @@ final class PreviewCoursePropertyManager extends BasicManager implements CourseP return (Property)propertyList.get(0); } + @Override + public List<Property> findCourseNodeProperties(CourseNode node, List<Identity> identities, String name) { + return Collections.emptyList(); + } + /** * @see org.olat.course.properties.CoursePropertyManager#deleteNodeProperties(org.olat.course.nodes.CourseNode, java.lang.String) */ diff --git a/src/main/java/org/olat/properties/NarrowedPropertyManager.java b/src/main/java/org/olat/properties/NarrowedPropertyManager.java index 1e01c33072e..eca30e22033 100644 --- a/src/main/java/org/olat/properties/NarrowedPropertyManager.java +++ b/src/main/java/org/olat/properties/NarrowedPropertyManager.java @@ -146,6 +146,20 @@ public class NarrowedPropertyManager { return pm.findProperty(identity, grp, resourceable, category, name); } + /** + * Find method for a batch of people + * + * @param identity + * @param grp + * @param category + * @param name + * @return The property or null if no property found + * @throws AssertException if more than one property matches. + */ + public List<Property> findProperties(List<Identity> identities, String category, String name) { + return pm.findProperties(identities, resourceable, category, name); + } + /** * deletes all properties of this resourceable * diff --git a/src/main/java/org/olat/properties/PropertyManager.java b/src/main/java/org/olat/properties/PropertyManager.java index 643c3d21d4d..758d02486ed 100644 --- a/src/main/java/org/olat/properties/PropertyManager.java +++ b/src/main/java/org/olat/properties/PropertyManager.java @@ -26,6 +26,7 @@ package org.olat.properties; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; @@ -34,6 +35,7 @@ import org.hibernate.Hibernate; import org.hibernate.type.Type; import org.olat.admin.user.delete.service.UserDeletionManager; import org.olat.core.commons.persistence.DBFactory; +import org.olat.core.commons.persistence.DBQuery; import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.AssertException; @@ -499,6 +501,42 @@ public class PropertyManager extends BasicManager implements UserDataDeletable { return (Property)props.get(0); } + public List<Property> findProperties(List<Identity> identities, OLATResourceable resourceable, String category, String name) { + if(identities == null || identities.isEmpty()) { + return Collections.emptyList(); + } + + StringBuilder query = new StringBuilder(); + query.append("select p from ").append(Property.class.getName()).append(" as p") + .append(" where p.identity in (:identities)"); + if (resourceable != null) { + query.append(" and p.resourceTypeName=:resourceTypeName and p.resourceTypeId=:resourceableId"); + } + if (category != null) { + query.append(" and p.category=:category"); + } + if (name != null) { + query.append(" and p.name=:name"); + } + + DBQuery dbQuery = DBFactory.getInstance().createQuery(query.toString()); + if (resourceable != null) { + dbQuery.setString("resourceTypeName", resourceable.getResourceableTypeName()); + dbQuery.setLong("resourceableId", resourceable.getResourceableId()); + } + if (category != null) { + dbQuery.setString("category", category); + } + if (name != null) { + dbQuery.setString("name", name); + } + dbQuery.setParameterList("identities", identities); + + List<Property> props = dbQuery.list(); + return props; + } + + /** * @return a list of all available resource type names */ -- GitLab