Index: lams_build/lib/lams/lams.jar =================================================================== diff -u -r7cce0b08560ba7d6ce7fa6470950f065bbb3cb46 -r18e768db89ac085de499f22cc961bed9a5785581 Binary files differ Index: lams_central/web/includes/javascript/progressBar.js =================================================================== diff -u -r9438790401b957cdd70a4d6ab2a76da70176c6eb -r18e768db89ac085de499f22cc961bed9a5785581 --- lams_central/web/includes/javascript/progressBar.js (.../progressBar.js) (revision 9438790401b957cdd70a4d6ab2a76da70176c6eb) +++ lams_central/web/includes/javascript/progressBar.js (.../progressBar.js) (revision 18e768db89ac085de499f22cc961bed9a5785581) @@ -21,6 +21,9 @@ var COLOR_COMPLEX_BACKGROUND = "rgb(153,153,153)"; // SVG paths for activity shapes +var PATH_START_LINE_HORIZONTAL = 'M 0 18 h 35'; +var PATH_START_LINE_VERTICAL = 'M 70 0 v 20'; + var PATH_SQUARE = " v16 h16 v-16 z"; var PATH_BIG_SQUARE = " v26 h26 v-26 z"; var PATH_CIRCLE = " m -8 0 a 8 8 0 1 0 16 0 a 8 8 0 1 0 -16 0"; @@ -40,6 +43,7 @@ + ",menubar=no, toolbar=no"); } +// just a short cut to openPopUp function function openActivity(url) { openPopUp(url, "LearnerActivity", 600, 800, "yes"); } @@ -55,14 +59,15 @@ var height = $(window).height(); if (hasContentFrame) { - // resize main content frame + // resize main content frame, if it is present $('#contentFrame').css({ 'width' : width + "px", 'height' : height + "px", 'position' : 'fixed' }); } + // do the real resizing; applicable only for Learner page if (!isHorizontalBar && progressPanelEnabled) { if (hasContentFrame && !controlFramePadding) { // calculate only once in the beginning @@ -199,16 +204,21 @@ act.decoration = paper.set(); // should be internationalised? + // and for some reason, in horizontal bars the text must be slightly shifted, why? var text = paper.text(act.middle, act.y + (isHorizontalBar ? 16 : 13), 'STOP'); + text.attr({ 'opacity' : 0, 'font-size' : 9, 'font' : 'sans-serif', 'stroke' : COLOR_GATE_TEXT, 'cursor' : 'pointer' }); + // correct Raphael bug + $('tspan', text.node).attr('dy', 0); act.decoration.push(text); - + + if (act.status == 0) { // add dark red edge when current activity act.statusTooltip = CURRENT_ACTIVITY_LABEL; @@ -292,6 +302,8 @@ 43 + 60 * activity.index + (isLarger ? 10 : 0), activity.name); } + // correct Raphael bug + $('tspan', label.node).attr('dy', 0); activity.elements.push(label); if (!isLast) { @@ -346,8 +358,7 @@ if (elem.decorationWraps) { // glow the wrapping decoration element // for example gray square in Optonal Activity container - // is bigger than inner activity shape, so it should - // glow + // is bigger than inner activity shape, so it should glow activity.shape.glowRef = elem.glow({ color : elem.attr('fill') }); @@ -770,18 +781,16 @@ this.isComplex = type == 'o' || (isPreview && type == 'b'); if (isHorizontalBar) { + // middle of the activity in X axis this.middle = 48 + 60 * index; this.y = 10; } else { // X positioning this.middle = 70; - // 20 is the first line segment and following activities take 60 px each - // (together with following vertical line) this.y = 20 + 60 * index; } - // first draw the inner shape, then put back the realY for background gray - // square + // first draw the inner shape, then put back the realY for background gray square var finalY = this.y; if (!isHorizontalBar && this.isComplex) { this.y += 5; @@ -869,7 +878,7 @@ function fillProgressBar(barId) { var bar = bars[barId]; if (!bar) { - // bar must be initialised first! + // bar should have been initialised first by another script return false; } @@ -883,9 +892,13 @@ cache : false, dataType : 'json', success : function(result) { + // check if container still exists + // it may happen if user quickly changes pages in Monitor + var barContainer = $('#' + bar.containerId); // if nothing changed, don't do any calculations - if (!bar.currentActivityId - || result.currentActivityId != bar.currentActivityId) { + if (barContainer.length > 0 + && (!bar.currentActivityId + || result.currentActivityId != bar.currentActivityId)) { bar.currentActivityId = result.currentActivityId; isPreview = result.isPreview; @@ -896,8 +909,7 @@ isHorizontalBar ? 40 + 60 * result.activities.length : 140, isHorizontalBar ? 60 : 60 * result.activities.length); // first line on the top - paper.path(isHorizontalBar ? 'M 0 18 h 35' - : 'M 70 0 v 20'); + paper.path(isHorizontalBar ? PATH_START_LINE_HORIZONTAL : PATH_START_LINE_VERTICAL); } // we need this to scroll to the current activity Index: lams_common/src/java/org/lamsfoundation/lams/lesson/util/LearnerProgressComparator.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/lesson/util/LearnerProgressComparator.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/lesson/util/LearnerProgressComparator.java (revision 18e768db89ac085de499f22cc961bed9a5785581) @@ -0,0 +1,56 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id$ */ +package org.lamsfoundation.lams.lesson.util; + +import java.util.Comparator; + +import org.lamsfoundation.lams.lesson.LearnerProgress; + +/** + * Compares two learner progress objects, setting more completed ones first. + * + * @author Marcin Cieslak + * + */ +public class LearnerProgressComparator implements Comparator { + + @Override + public int compare(LearnerProgress o1, LearnerProgress o2) { + if (o1 == null) { + return o2 == null ? 0 : -1; + } + if (o2 == null) { + return 1; + } + + if (o1.isComplete()) { + return o2.isComplete() ? 0 : -1; + } + if (o2.isComplete()) { + return 1; + } + + return new Integer(o2.getCompletedActivities().size()).compareTo(o1.getCompletedActivities().size()); + } +} Index: lams_common/src/java/org/lamsfoundation/lams/lesson/util/LearnerProgressNameComparator.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/lesson/util/LearnerProgressNameComparator.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/lesson/util/LearnerProgressNameComparator.java (revision 18e768db89ac085de499f22cc961bed9a5785581) @@ -0,0 +1,52 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id$ */ +package org.lamsfoundation.lams.lesson.util; + +import java.util.Comparator; + +import org.lamsfoundation.lams.lesson.LearnerProgress; + +/** + * Compares two learner progress objects, based on user name. + * + * @author Marcin Cieslak + * + */ +public class LearnerProgressNameComparator implements Comparator { + + @Override + public int compare(LearnerProgress o1, LearnerProgress o2) { + if (o1 == null) { + return o2 == null ? 0 : -1; + } + if (o2 == null) { + return 1; + } + + String o1Name = o1.getUser().getFirstName() + o1.getUser().getLastName(); + String o2Name = o2.getUser().getFirstName() + o2.getUser().getLastName(); + return o1Name.compareTo(o2Name); + } + +} Index: lams_monitoring/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r9438790401b957cdd70a4d6ab2a76da70176c6eb -r18e768db89ac085de499f22cc961bed9a5785581 --- lams_monitoring/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 9438790401b957cdd70a4d6ab2a76da70176c6eb) +++ lams_monitoring/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 18e768db89ac085de499f22cc961bed9a5785581) @@ -286,5 +286,7 @@ button.export.learner.tooltip =Export this learner portfolio and save it on your computer for future reference button.timechart =View Time Chart button.timechart.tooltip =View a chart of the selected learner progress against time for each activity +lesson.learners.page =Page +lesson.learners.order =Order by completion #======= End labels: Exported 196 labels for en AU ===== Index: lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/MonitoringAction.java =================================================================== diff -u -r570505035381ff9b0640e56bb12c954ecf4166fa -r18e768db89ac085de499f22cc961bed9a5785581 --- lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/MonitoringAction.java (.../MonitoringAction.java) (revision 570505035381ff9b0640e56bb12c954ecf4166fa) +++ lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/MonitoringAction.java (.../MonitoringAction.java) (revision 18e768db89ac085de499f22cc961bed9a5785581) @@ -30,6 +30,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -58,6 +59,8 @@ import org.lamsfoundation.lams.lesson.Lesson; import org.lamsfoundation.lams.lesson.dto.LessonDetailsDTO; import org.lamsfoundation.lams.lesson.service.ILessonService; +import org.lamsfoundation.lams.lesson.util.LearnerProgressComparator; +import org.lamsfoundation.lams.lesson.util.LearnerProgressNameComparator; import org.lamsfoundation.lams.lesson.util.LessonComparator; import org.lamsfoundation.lams.monitoring.MonitoringConstants; import org.lamsfoundation.lams.monitoring.dto.ContributeActivityDTO; @@ -71,6 +74,8 @@ import org.lamsfoundation.lams.usermanagement.dto.UserDTO; import org.lamsfoundation.lams.usermanagement.exception.UserAccessDeniedException; import org.lamsfoundation.lams.usermanagement.service.IUserManagementService; +import org.lamsfoundation.lams.util.Configuration; +import org.lamsfoundation.lams.util.ConfigurationKeys; import org.lamsfoundation.lams.util.DateUtil; import org.lamsfoundation.lams.util.MessageService; import org.lamsfoundation.lams.util.WebUtil; @@ -1087,6 +1092,9 @@ return null; } + /** + * Displays Monitor Lesson page. + */ public ActionForward monitorLesson(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Long lessonId = WebUtil.readLongParam(request, AttributeNames.PARAM_LESSON_ID); @@ -1130,14 +1138,72 @@ return mapping.findForward("monitorLesson"); } + /** + * Gets users whose progress bars will be displayed in Learner tab in Monitor. + */ @SuppressWarnings("unchecked") + public ActionForward getLearnerProgressPageJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws JSONException, IOException { + long lessonId = WebUtil.readLongParam(request, AttributeNames.PARAM_LESSON_ID); + String searchPhrase = request.getParameter("searchPhrase"); + Integer pageNumber = WebUtil.readIntParam(request, "pageNumber", true); + if (pageNumber == null) { + pageNumber = 1; + } + boolean isProgressSorted = WebUtil.readBooleanParam(request, "isProgressSorted", false); + + JSONObject responseJSON = new JSONObject(); + Lesson lesson = getLessonService().getLesson(lessonId); + List learnerProgresses = new ArrayList(lesson.getLearnerProgresses()); + // sort either by user's name or his progress + Collections.sort(learnerProgresses, isProgressSorted ? new LearnerProgressComparator() + : new LearnerProgressNameComparator()); + + if (!StringUtils.isBlank(searchPhrase)) { + searchPhrase = searchPhrase.toLowerCase(); + + // get only users whose names match the given phrase + List searchResult = new ArrayList(); + for (LearnerProgress learnerProgress : learnerProgresses) { + User learner = learnerProgress.getUser(); + StringBuilder learnerDisplayName = new StringBuilder(learner.getFirstName().toLowerCase()).append(" ") + .append(learner.getLastName().toLowerCase()).append(" ") + .append(learner.getLogin().toLowerCase()); + if (learnerDisplayName.indexOf(searchPhrase) != -1) { + searchResult.add(learnerProgress); + } + } + + learnerProgresses = searchResult; + } + + // batch size is 10 + int fromIndex = (pageNumber - 1) * 10; + int toIndex = Math.min(pageNumber * 10, learnerProgresses.size()); + // get just the requested chunk + for (LearnerProgress learnerProgress : learnerProgresses.subList(fromIndex, toIndex)) { + responseJSON.append("learners", MonitoringAction.userToJSON(learnerProgress.getUser())); + } + + responseJSON.put("numberActiveLearners", learnerProgresses.size()); + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(responseJSON.toString()); + return null; + } + + /** + * Produces necessary data for learner progress bar. + */ + @SuppressWarnings("unchecked") public ActionForward getLearnerProgressJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws JSONException, IOException { Integer learnerId = WebUtil.readIntParam(request, AttributeNames.PARAM_USER_ID, true); Integer monitorId = null; if (learnerId == null) { + // get progress for current user learnerId = getUserId(); } else { + // monitor mode; get progress for user given in the parameter monitorId = getUserId(); } @@ -1152,7 +1218,8 @@ if (activity.getFloating()) { // these are support activities for (ActivityURL childActivity : activity.getChildActivities()) { - responseJSON.append("support", activityProgressToJSON(childActivity, null, lessonId, learnerId, monitorId)); + responseJSON.append("support", + activityProgressToJSON(childActivity, null, lessonId, learnerId, monitorId)); } } else { responseJSON.append("activities", @@ -1166,21 +1233,22 @@ return null; } - @SuppressWarnings("unchecked") - public ActionForward getLessonProgressJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) throws JSONException, IOException { + /** + * Produces data to update Lesson tab in Monitor. + */ + public ActionForward getLessonDetailsJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws IOException, JSONException { long lessonId = WebUtil.readLongParam(request, AttributeNames.PARAM_LESSON_ID); + + JSONObject responseJSON = new JSONObject(); Lesson lesson = getLessonService().getLesson(lessonId); LessonDetailsDTO lessonDetails = lesson.getLessonDetails(); String contentFolderId = lessonDetails.getContentFolderID(); - IMonitoringService monitoringService = MonitoringServiceProxy.getMonitoringService(getServlet() - .getServletContext()); - Integer monitorUserId = getUserId(); + HttpSession ss = SessionManager.getSession(); UserDTO user = (UserDTO) ss.getAttribute(AttributeNames.USER); Locale userLocale = new Locale(user.getLocaleLanguage(), user.getLocaleCountry()); - JSONObject responseJSON = new JSONObject(); responseJSON.put("numberPossibleLearners", lessonDetails.getNumberPossibleLearners()); responseJSON.put("lessonStateID", lessonDetails.getLessonStateID()); @@ -1193,6 +1261,24 @@ indfm.format(tzStartDate) + " " + user.getTimeZone().getDisplayName(userLocale)); } + response.getWriter().write(responseJSON.toString()); + return null; + } + + /** + * Produces data for Sequence tab in Monitor. + */ + @SuppressWarnings("unchecked") + public ActionForward getLessonProgressJSON(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws JSONException, IOException { + long lessonId = WebUtil.readLongParam(request, AttributeNames.PARAM_LESSON_ID); + Lesson lesson = getLessonService().getLesson(lessonId); + IMonitoringService monitoringService = MonitoringServiceProxy.getMonitoringService(getServlet() + .getServletContext()); + Integer monitorUserId = getUserId(); + LessonDetailsDTO lessonDetails = lesson.getLessonDetails(); + String contentFolderId = lessonDetails.getContentFolderID(); + // few details for each activity Map activitiesMap = new TreeMap(); for (Activity activity : (Set) lesson.getLearningDesign().getActivities()) { @@ -1202,15 +1288,16 @@ String monitorUrl = monitoringService.getActivityMonitorURL(lessonId, activityId, contentFolderId, monitorUserId); if (monitorUrl != null) { + // whole activity monitor URL activityJSON.put("url", monitorUrl); } activitiesMap.put(activityId, activityJSON); } + JSONObject responseJSON = new JSONObject(); for (LearnerProgress learnerProgress : (Set) lesson.getLearnerProgresses()) { User learner = learnerProgress.getUser(); JSONObject learnerJSON = MonitoringAction.userToJSON(learner); - responseJSON.append("learners", learnerJSON); if (learnerProgress.isComplete()) { // no more details are needed for learners who completed the lesson responseJSON.append("completedLearners", learnerJSON); @@ -1230,6 +1317,7 @@ } responseJSON.put("activities", new JSONArray(activitiesMap.values())); + responseJSON.put("numberPossibleLearners", lessonDetails.getNumberPossibleLearners()); response.getWriter().write(responseJSON.toString()); return null; @@ -1527,21 +1615,32 @@ return userJSON; } - private JSONObject activityProgressToJSON(ActivityURL activity, Long currentActivityId, Long lessonId, Integer learnerId, - Integer monitorId) throws JSONException, IOException { + /** + * Converts an activity in learner progress to a JSON object. + */ + private JSONObject activityProgressToJSON(ActivityURL activity, Long currentActivityId, Long lessonId, + Integer learnerId, Integer monitorId) throws JSONException, IOException { JSONObject activityJSON = new JSONObject(); activityJSON.put("id", activity.getActivityId()); activityJSON.put("name", activity.getTitle()); activityJSON.put("status", activity.getActivityId().equals(currentActivityId) ? 0 : activity.getStatus()); + // URL in learner mode String url = activity.getUrl(); - if (url != null && monitorId != null) { + if ((url != null) && (monitorId != null)) { IMonitoringService monitoringService = MonitoringServiceProxy.getMonitoringService(getServlet() .getServletContext()); + // URL in monitor mode url = monitoringService.getLearnerActivityURL(lessonId, activity.getActivityId(), learnerId, monitorId); } + if (url != null) { - activityJSON.put("url", activity.getUrl()); + String serverUrl = Configuration.get(ConfigurationKeys.SERVER_URL); + if (!url.startsWith(serverUrl)) { + // monitor mode URLs should be prepended with serve URL + url = serverUrl + url; + } + activityJSON.put("url", url); } String actType = activity.getType().toLowerCase(); @@ -1553,7 +1652,6 @@ } else if (actType.contains("branching")) { type = "b"; } - activityJSON.put("type", type); if (activity.getChildActivities() != null) { Index: lams_monitoring/web/css/monitorLesson.css =================================================================== diff -u -r570505035381ff9b0640e56bb12c954ecf4166fa -r18e768db89ac085de499f22cc961bed9a5785581 --- lams_monitoring/web/css/monitorLesson.css (.../monitorLesson.css) (revision 570505035381ff9b0640e56bb12c954ecf4166fa) +++ lams_monitoring/web/css/monitorLesson.css (.../monitorLesson.css) (revision 18e768db89ac085de499f22cc961bed9a5785581) @@ -183,6 +183,37 @@ } /********** LEARNERS TAB STYLES **********/ +#tabLearnerControlTable td { + padding: 0px; + text-align: center; +} + +td.learnersHeaderCell { + border-right: thin solid #2E6E9E; +} + +#tabLearnerControlTable td.topButtonsContainer { + width: 90px; +} + +td.learnersHeaderPageCell,td.learnersPageShifter { + width: 30px; + cursor: pointer; +} + +td.learnersHeaderPageCell:hover,td.learnersPageShifter:hover { + background-color: #D0E5F5; +} + +td.selectedLearnersHeaderPageCell { + background-color: #5c9ccc; +} + +div#tabLearnersContainer { + height: 500px; + overflow: auto; +} + td.progressBarLabel { padding: 0; background-color: #D0E5F5; Index: lams_monitoring/web/includes/javascript/monitorLesson.js =================================================================== diff -u -r570505035381ff9b0640e56bb12c954ecf4166fa -r18e768db89ac085de499f22cc961bed9a5785581 --- lams_monitoring/web/includes/javascript/monitorLesson.js (.../monitorLesson.js) (revision 570505035381ff9b0640e56bb12c954ecf4166fa) +++ lams_monitoring/web/includes/javascript/monitorLesson.js (.../monitorLesson.js) (revision 18e768db89ac085de499f22cc961bed9a5785581) @@ -1,6 +1,6 @@ // ********** GLOBAL VARIABLES ********** -var refreshInProgress = false; // copy of lesson SVG so it does no need to be fetched every time +// HTML with SVG of the lesson var originalSequenceCanvas = null; // DIV container for lesson SVG; it gets accessed so many times it's worth to cache it here var sequenceCanvas = null; @@ -10,8 +10,19 @@ classLearner : false, classMonitor : false }; -var bars = {}; +// container for learners' progress bars metadata +var bars = null; +// placeholder for single learner's progress bar and title +var learnerProgressCellsTemplate = null; +// for synchronisation purposes +var learnersRefreshInProgress = false; +var sequenceRefreshInProgress = false; +// total number of learners with ongoing progress +var numberActiveLearners = 0; +// page in Learners tab +var learnerProgressCurrentPageNumber = 1; + //********** LESSON TAB FUNCTIONS ********** /** @@ -127,7 +138,7 @@ }, success : function() { dialog.dialog('close'); - refreshMonitor(); + refreshMonitor('lesson'); } }); } @@ -218,7 +229,7 @@ // user chose to finish the lesson, close monitoring and refresh the lesson list window.parent.closeMonitorLessonDialog(true); } else { - refreshMonitor(); + refreshMonitor('lesson'); } } }); @@ -229,75 +240,87 @@ /** * Updates widgets in lesson tab according to respose sent to refreshMonitor() */ -function updateLessonTab(refreshResponse){ - // update lesson state label - lessonStateId = +refreshResponse.lessonStateID; - var label = null; - switch (lessonStateId) { - case 1: - label = LESSON_STATE_CREATED_LABEL; - break; - case 2: - label = LESSON_STATE_SCHEDULED_LABEL; - break; - case 3: - label = LESSON_STATE_STARTED_LABEL; - break; - case 4: - label = LESSON_STATE_SUSPENDED_LABEL; - break; - case 5: - label = LESSON_STATE_FINISHED_LABEL; - break; - case 6: - label = LESSON_STATE_ARCHIVED_LABEL; - break; - case 7: - label = LESSON_STATE_REMOVED_LABEL; - break; - } - $('#lessonStateLabel').text(label); - - // update available options in change state dropdown menu - var selectField = $('#lessonStateField'); - // remove all except "Select status" option - selectField.children('option:not([value="-1"])').remove(); - switch (lessonStateId) { - case 3: - $('