/********************************************************************************** * * $Id: EnrollmentTableBean.java 121717 2013-03-25 15:55:01Z bkirschn@umich.edu $ * *********************************************************************************** * * Copyright (c) 2005, 2006, 2007, 2008 The Sakai Foundation, The MIT Corporation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.tool.gradebook.ui; import java.io.Serializable; import java.text.Collator; import java.text.ParseException; import java.text.RuleBasedCollator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.faces.context.FacesContext; import javax.faces.event.ActionEvent; import javax.faces.event.ValueChangeEvent; import javax.faces.model.SelectItem; import javax.faces.event.ValueChangeEvent; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.section.api.coursemanagement.CourseSection; import org.sakaiproject.section.api.coursemanagement.EnrollmentRecord; import org.sakaiproject.service.gradebook.shared.UnknownUserException; import org.sakaiproject.tool.gradebook.Category; import org.sakaiproject.tool.gradebook.GradingEvent; import org.sakaiproject.tool.gradebook.jsf.FacesUtil; /** * This is an abstract base class for gradebook dependent backing * beans that support searching, sorting, and paging student data. */ public abstract class EnrollmentTableBean extends GradebookDependentBean implements Paging, Serializable { private static final Log log = LogFactory.getLog(EnrollmentTableBean.class); /** * A comparator that sorts enrollments by student sortName */ static final Comparator<EnrollmentRecord> ENROLLMENT_NAME_COMPARATOR = new Comparator<EnrollmentRecord>() { Collator collator; { collator = Collator.getInstance(); try { collator= new RuleBasedCollator(((RuleBasedCollator) collator).getRules().replaceAll("<'\u005f'", "<' '<'\u005f'")); } catch (ParseException e) { log.warn(this + " Cannot init RuleBasedCollator. Will use the default Collator instead.", e); } } public int compare(EnrollmentRecord o1, EnrollmentRecord o2) { return collator.compare(o1.getUser().getSortName(), o2.getUser().getSortName()); } }; /** * A comparator that sorts enrollments by student display UID (for installations * where a student UID is not a number) */ static final Comparator<EnrollmentRecord> ENROLLMENT_DISPLAY_UID_COMPARATOR = new Comparator<EnrollmentRecord>() { public int compare(EnrollmentRecord o1, EnrollmentRecord o2) { return o1.getUser().getDisplayId().compareToIgnoreCase(o2.getUser().getDisplayId()); } }; /** * A comparator that sorts enrollments by student display UID (for installations * where a student UID is a number) */ static final Comparator<EnrollmentRecord> ENROLLMENT_DISPLAY_UID_NUMERIC_COMPARATOR = new Comparator<EnrollmentRecord>() { public int compare(EnrollmentRecord o1, EnrollmentRecord o2) { long user1DisplayId = Long.parseLong(o1.getUser().getDisplayId()); long user2DisplayId = Long.parseLong(o2.getUser().getDisplayId()); return (int)(user1DisplayId - user2DisplayId); } }; public static final int ALL_SECTIONS_SELECT_VALUE = -1; public static final int ALL_CATEGORIES_SELECT_VALUE = -1; private static Map columnSortMap; private String searchString; private int firstScoreRow; public int maxDisplayedScoreRows; private int scoreDataRows; private boolean emptyEnrollments; // Needed to render buttons private String defaultSearchString; private boolean refreshRoster=true; // To prevent unnecessary roster loading // The section selection menu will include some choices that aren't // real sections (e.g., "All Sections" or "Unassigned Students". private Integer selectedSectionFilterValue = new Integer(ALL_SECTIONS_SELECT_VALUE); private List sectionFilterSelectItems; private List availableSections; // The real sections accessible by this user // The category selection menu will include some choices that aren't // real categories (e.g., "All Categories") and will only be rendered if // categories exists. private Integer selectedCategoryFilterValue = new Integer(ALL_CATEGORIES_SELECT_VALUE); private List availableCategories; private List categoryFilterSelectItems; // We only store grader UIDs in the grading event history, but the // log displays grader names instead. This map cuts down on possibly expensive // calls to the user directory service. private Map graderIdToNameMap; public EnrollmentTableBean() { maxDisplayedScoreRows = getPreferencesBean().getDefaultMaxDisplayedScoreRows(); } static { columnSortMap = new HashMap(); columnSortMap.put(PreferencesBean.SORT_BY_NAME, ENROLLMENT_NAME_COMPARATOR); columnSortMap.put(PreferencesBean.SORT_BY_UID, ENROLLMENT_DISPLAY_UID_COMPARATOR); } // Searching public String getSearchString() { return searchString; } public void setSearchString(String searchString) { if (StringUtils.trimToNull(searchString) == null) { searchString = defaultSearchString; } if (!StringUtils.equals(searchString, this.searchString)) { if (log.isDebugEnabled()) log.debug("setSearchString " + searchString); this.searchString = searchString; setFirstRow(0); // clear the paging when we update the search } } public void search(ActionEvent event) { // We don't need to do anything special here, since init will handle the search if (log.isDebugEnabled()) log.debug("search"); setRefreshRoster(true); } public void clear(ActionEvent event) { if (log.isDebugEnabled()) log.debug("clear"); setSearchString(null); setRefreshRoster(true); } // Sorting public void sort(ActionEvent event) { setFirstRow(0); // clear the paging whenever we update the sorting setRefreshRoster(true); } public abstract boolean isSortAscending(); public abstract void setSortAscending(boolean sortAscending); public abstract String getSortColumn(); public abstract void setSortColumn(String sortColumn); // Paging. /** * This method more or less turns a JSF input component (namely the Sakai Pager tag) * into a JSF command component. We want the Pager to cause immediate pseudo-navigation * to a new state, throwing away any score input values without bothering to * validate them. But because the Pager is a UIInput component, it doesn't * have an action method that will be called before full validation is done. * Instead, we declare our Pager input tag to be immediate, set this * valueChangeListener, and explicitly jump over all other intervening * phases directly to the rendering phase, which should then pick up the new paging * values. */ public void changePagingState(ValueChangeEvent valueChange) { if (log.isDebugEnabled()) log.debug("changePagingState: old=" + valueChange.getOldValue() + ", new=" + valueChange.getNewValue()); FacesContext.getCurrentInstance().renderResponse(); } public int getFirstRow() { if (log.isDebugEnabled()) log.debug("getFirstRow " + firstScoreRow); return firstScoreRow; } public void setFirstRow(int firstRow) { if (log.isDebugEnabled()) log.debug("setFirstRow from " + firstScoreRow + " to " + firstRow); firstScoreRow = firstRow; setRefreshRoster(true); } public int getMaxDisplayedRows() { return maxDisplayedScoreRows; } public void setMaxDisplayedRows(int maxDisplayedRows) { maxDisplayedScoreRows = maxDisplayedRows; setRefreshRoster(true); } public int getDataRows() { return scoreDataRows; } private boolean isFilteredSearch() { return !StringUtils.equals(searchString, defaultSearchString); } /** * * @return Map of EnrollmentRecord to Map of gbItems and function (grade/view). * This is the group of students viewable on the roster page */ protected Map getOrderedEnrollmentMapForAllItems() { Map enrollments = getWorkingEnrollmentsForAllItems(); scoreDataRows = enrollments.size(); emptyEnrollments = enrollments.isEmpty(); return transformToOrderedEnrollmentMapForAllItems(enrollments); } /** * * @return Map of studentId to EnrollmentRecord in order. This is the group of * students viewable for the course grade page */ protected Map getOrderedEnrollmentMapForCourseGrades() { Map enrollments = getWorkingEnrollmentsForCourseGrade(); scoreDataRows = enrollments.size(); emptyEnrollments = enrollments.isEmpty(); return transformToOrderedEnrollmentMapWithFunction(enrollments); } /** * * @param itemId * @param categoryId * @return Map of studentId to EnrollmentRecord in order. This is the group of * students viewable for a particular category associated with gb item */ protected Map getOrderedEnrollmentMapForItem(Long categoryId) { Map enrollments = getWorkingEnrollmentsForItem(categoryId); scoreDataRows = enrollments.size(); emptyEnrollments = enrollments.isEmpty(); return transformToOrderedEnrollmentMapWithFunction(enrollments); } /** * * @param categoryId - optional category filter * @return Map of studentId to EnrollmentRecord in order. */ protected Map getOrderedStudentIdEnrollmentMapForItem(Long categoryId) { Map enrollments = getWorkingEnrollmentsForItem(categoryId); scoreDataRows = enrollments.size(); emptyEnrollments = enrollments.isEmpty(); return transformToOrderedEnrollmentMap(new ArrayList(enrollments.keySet())); } protected Map getWorkingEnrollmentsForAllItems() { Map enrollments; String selSearchString = null; if (isFilteredSearch()) { selSearchString = searchString; } String selSectionUid = null; if (!isAllSectionsSelected()) { selSectionUid = getSelectedSectionUid(); } enrollments = findMatchingEnrollmentsForAllItems(selSearchString, selSectionUid); return enrollments; } protected Map getWorkingEnrollmentsForItem(Long categoryId) { Map enrollments; String selSearchString = null; if (isFilteredSearch()) { selSearchString = searchString; } String selSectionUid = null; if (!isAllSectionsSelected()) { selSectionUid = getSelectedSectionUid(); } enrollments = findMatchingEnrollmentsForItem(categoryId, selSearchString, selSectionUid); return enrollments; } /** * * @return Map of EnrollmentRecord --> function (view/grade) that the current user * has grade/view permission for every gb item */ protected Map getWorkingEnrollmentsForCourseGrade() { Map enrollments; String selSearchString = null; if (isFilteredSearch()) { selSearchString = searchString; } String selSectionUid = null; if (!isAllSectionsSelected()) { selSectionUid = getSelectedSectionUid(); } enrollments = findMatchingEnrollmentsForViewableCourseGrade(selSearchString, selSectionUid); return enrollments; } /** * * @param enrollmentList - list of EnrollmentRecords * @return Ordered Map of student Id to EnrollmentRecord */ private Map transformToOrderedEnrollmentMap(List enrollmentList) { Map enrollmentMap; if (isEnrollmentSort()) { Collections.sort(enrollmentList, (Comparator)columnSortMap.get(getSortColumn())); enrollmentList = finalizeSortingAndPaging(enrollmentList); enrollmentMap = new LinkedHashMap(); // Preserve ordering } else { enrollmentMap = new HashMap(); } for (Iterator iter = enrollmentList.iterator(); iter.hasNext(); ) { EnrollmentRecord enr = (EnrollmentRecord)iter.next(); enrollmentMap.put(enr.getUser().getUserUid(), enr); } return enrollmentMap; } /** * * @param enrollmentMap - map of EnrollmentRecord --> function * @return Ordered Map of student Id to map of EnrollmentRecord to function */ private Map transformToOrderedEnrollmentMapWithFunction(Map enrRecFunctionMap) { Map studentIdEnrRecFunctionMap; Map enrollmentMap; List enrollmentList = new ArrayList(enrRecFunctionMap.keySet()); if (isEnrollmentSort()) { Collections.sort(enrollmentList, (Comparator)columnSortMap.get(getSortColumn())); enrollmentList = finalizeSortingAndPaging(enrollmentList); enrollmentMap = new LinkedHashMap(); // Preserve ordering } else { enrollmentMap = new HashMap(); } for (Iterator iter = enrollmentList.iterator(); iter.hasNext(); ) { EnrollmentRecord enr = (EnrollmentRecord)iter.next(); Map newEnrRecFunctionMap = new HashMap(); newEnrRecFunctionMap.put(enr, enrRecFunctionMap.get(enr)); enrollmentMap.put(enr.getUser().getUserUid(), newEnrRecFunctionMap); } return enrollmentMap; } /** * * @param enrollmentMapAllItems * @return an ordered map of EnrollmentRecords to a map of gb Items to function (grade/view) */ private Map transformToOrderedEnrollmentMapForAllItems(Map enrollmentMapAllItems) { Map enrollmentMap; List enrRecList = new ArrayList(enrollmentMapAllItems.keySet()); if (isEnrollmentSort()) { Collections.sort(enrRecList, (Comparator)columnSortMap.get(getSortColumn())); enrRecList = finalizeSortingAndPaging(enrRecList); enrollmentMap = new LinkedHashMap(); // Preserve ordering } else { enrollmentMap = new HashMap(); } for (Iterator iter = enrRecList.iterator(); iter.hasNext(); ) { EnrollmentRecord enr = (EnrollmentRecord)iter.next(); enrollmentMap.put(enr, (Map)enrollmentMapAllItems.get(enr)); } return enrollmentMap; } protected List finalizeSortingAndPaging(List list) { List finalList; if (!isSortAscending()) { Collections.reverse(list); } if (maxDisplayedScoreRows == 0) { finalList = list; } else { int nextPageRow = Math.min(firstScoreRow + maxDisplayedScoreRows, scoreDataRows); finalList = new ArrayList(list.subList(firstScoreRow, nextPageRow)); if (log.isDebugEnabled()) log.debug("finalizeSortingAndPaging subList " + firstScoreRow + ", " + nextPageRow); } return finalList; } public boolean isEnrollmentSort() { String sortColumn = getSortColumn(); return (sortColumn.equals(PreferencesBean.SORT_BY_NAME) || sortColumn.equals(PreferencesBean.SORT_BY_UID)); } protected void init() { graderIdToNameMap = new HashMap(); defaultSearchString = getLocalizedString("search_default_student_search_string"); if (searchString == null) { searchString = defaultSearchString; } // Section filtering. availableSections = getViewableSections(); sectionFilterSelectItems = new ArrayList(); // The first choice is always "All available enrollments" sectionFilterSelectItems.add(new SelectItem(new Integer(ALL_SECTIONS_SELECT_VALUE), FacesUtil.getLocalizedString("search_sections_all"))); // TODO If there are unassigned students and the current user is allowed to see them, add them next. // Add the available sections. for (int i = 0; i < availableSections.size(); i++) { CourseSection section = (CourseSection)availableSections.get(i); sectionFilterSelectItems.add(new SelectItem(new Integer(i), section.getTitle())); } // If the selected value now falls out of legal range due to sections // being deleted, throw it back to the default value (meaning everyone). int selectedSectionVal = selectedSectionFilterValue.intValue(); if ((selectedSectionVal >= 0) && (selectedSectionVal >= availableSections.size())) { if (log.isInfoEnabled()) log.info("selectedSectionFilterValue=" + selectedSectionFilterValue.intValue() + " but available sections=" + availableSections.size()); selectedSectionFilterValue = new Integer(ALL_SECTIONS_SELECT_VALUE); } // Category filtering availableCategories = getViewableCategories(); categoryFilterSelectItems = new ArrayList(); // The first choice is always "All Categories" categoryFilterSelectItems.add(new SelectItem(new Integer(ALL_CATEGORIES_SELECT_VALUE), FacesUtil.getLocalizedString("search_categories_all"))); // Add available categories for (int i=0; i < availableCategories.size(); i++){ Category cat = (Category) availableCategories.get(i); categoryFilterSelectItems.add(new SelectItem(new Integer(cat.getId().intValue()), cat.getName())); } // If the selected value now falls out of legal range due to categories // being deleted, throw it back to the default value (meaning all categories) int selectedCategoryVal = selectedCategoryFilterValue.intValue(); } public boolean isAllSectionsSelected() { return (selectedSectionFilterValue.intValue() == ALL_SECTIONS_SELECT_VALUE); } public String getSelectedSectionUid() { int filterValue = selectedSectionFilterValue.intValue(); if (filterValue == ALL_SECTIONS_SELECT_VALUE) { return null; } else { if (availableSections == null) availableSections = getViewableSections(); CourseSection section = (CourseSection)availableSections.get(filterValue); return section.getUuid(); } } public Integer getSelectedSectionFilterValue() { return selectedSectionFilterValue; } public void setSelectedSectionFilterValue(Integer selectedSectionFilterValue) { if (!selectedSectionFilterValue.equals(this.selectedSectionFilterValue)) { this.selectedSectionFilterValue = selectedSectionFilterValue; setFirstRow(0); // clear the paging when we update the search setRefreshRoster(true); } } public List getSectionFilterSelectItems() { return sectionFilterSelectItems; } public boolean isAllCategoriesSelected() { return (selectedCategoryFilterValue.intValue() == ALL_CATEGORIES_SELECT_VALUE); } public String getSelectedCategoryUid() { int filterValue = selectedCategoryFilterValue.intValue(); if (filterValue == ALL_CATEGORIES_SELECT_VALUE) { return null; } else { return Integer.toString(filterValue); //Category cat = (Category) availableCategories.get(filterValue); //return cat.getId().toString(); } } public String getCategoryUid(String uid){ if (uid == null) { return null; } Integer Uid = new Integer(uid); if (Uid == ALL_CATEGORIES_SELECT_VALUE) { return null; } else { return uid; } } public Integer getSelectedCategoryFilterValue() { return selectedCategoryFilterValue; } public void setSelectedCategoryFilterValue(Integer selectedCategoryFilterValue) { if (!selectedCategoryFilterValue.equals(this.selectedCategoryFilterValue)) { this.selectedCategoryFilterValue = selectedCategoryFilterValue; setFirstRow(0); // clear the paging when we update the search setRefreshRoster(true); } } public void setSelectedCategoryFilterValue(ValueChangeEvent event){ Integer newValue = (Integer) event.getNewValue(); if (!newValue.equals(this.selectedCategoryFilterValue)) { this.selectedCategoryFilterValue = newValue; setFirstRow(0); // clear the paging when we update the search setRefreshRoster(true); } } public List getCategoryFilterSelectItems() { return categoryFilterSelectItems; } public boolean isEmptyEnrollments() { return emptyEnrollments; } public boolean isRefreshRoster() { return refreshRoster; } public void setRefreshRoster(boolean refreshRoster) { this.refreshRoster = refreshRoster; } // Map grader UIDs to grader names for the grading event log. public String getGraderNameForId(String graderId) { if (graderIdToNameMap == null) graderIdToNameMap = new HashMap(); String graderName = (String)graderIdToNameMap.get(graderId); if (graderName == null) { try { graderName = getUserDirectoryService().getUserDisplayName(graderId); } catch (UnknownUserException e) { log.warn("Unable to find grader with uid=" + graderId); graderName = graderId; } graderIdToNameMap.put(graderId, graderName); } return graderName; } // Support grading event logs. public class GradingEventRow implements Serializable { private Date date; private String graderName; private String grade; public GradingEventRow(GradingEvent gradingEvent) { date = gradingEvent.getDateGraded(); grade = gradingEvent.getGrade(); graderName = getGraderNameForId(gradingEvent.getGraderId()); } public Date getDate() { return date; } public String getGrade() { if (grade != null) { try { Double gradeDouble = new Double(grade); // we may have gained decimal places in the conversion from points to % grade = FacesUtil.getRoundDown(gradeDouble.doubleValue(), 2) + ""; } catch (NumberFormatException nfe) { // ignore b/c may be letter grade } } return grade; } public String getGraderName() { return graderName; } } }