001: /**********************************************************************************
002: *
003: * $Id: EnrollmentTableBean.java 29605 2007-04-26 14:01:18Z ajpoland@iupui.edu $
004: *
005: ***********************************************************************************
006: *
007: * Copyright (c) 2005 The Regents of the University of California, The MIT Corporation
008: *
009: * Licensed under the Educational Community License, Version 1.0 (the "License");
010: * you may not use this file except in compliance with the License.
011: * You may obtain a copy of the License at
012: *
013: * http://www.opensource.org/licenses/ecl1.php
014: *
015: * Unless required by applicable law or agreed to in writing, software
016: * distributed under the License is distributed on an "AS IS" BASIS,
017: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018: * See the License for the specific language governing permissions and
019: * limitations under the License.
020: *
021: **********************************************************************************/package org.sakaiproject.tool.gradebook.ui;
022:
023: import java.io.Serializable;
024: import java.util.ArrayList;
025: import java.util.Collections;
026: import java.util.Comparator;
027: import java.util.Date;
028: import java.util.HashMap;
029: import java.util.Iterator;
030: import java.util.LinkedHashMap;
031: import java.util.List;
032: import java.util.Map;
033:
034: import javax.faces.context.FacesContext;
035: import javax.faces.event.ActionEvent;
036: import javax.faces.event.ValueChangeEvent;
037: import javax.faces.model.SelectItem;
038:
039: import org.apache.commons.lang.StringUtils;
040: import org.apache.commons.logging.Log;
041: import org.apache.commons.logging.LogFactory;
042: import org.sakaiproject.section.api.coursemanagement.CourseSection;
043: import org.sakaiproject.section.api.coursemanagement.EnrollmentRecord;
044: import org.sakaiproject.service.gradebook.shared.UnknownUserException;
045: import org.sakaiproject.tool.gradebook.GradingEvent;
046: import org.sakaiproject.tool.gradebook.jsf.FacesUtil;
047:
048: /**
049: * This is an abstract base class for gradebook dependent backing
050: * beans that support searching, sorting, and paging student data.
051: */
052: public abstract class EnrollmentTableBean extends
053: GradebookDependentBean implements Paging, Serializable {
054: private static final Log log = LogFactory
055: .getLog(EnrollmentTableBean.class);
056:
057: /**
058: * A comparator that sorts enrollments by student sortName
059: */
060: static final Comparator<EnrollmentRecord> ENROLLMENT_NAME_COMPARATOR = new Comparator<EnrollmentRecord>() {
061: public int compare(EnrollmentRecord o1, EnrollmentRecord o2) {
062: return o1.getUser().getSortName().compareToIgnoreCase(
063: o2.getUser().getSortName());
064: }
065: };
066:
067: /**
068: * A comparator that sorts enrollments by student display UID (for installations
069: * where a student UID is not a number)
070: */
071: static final Comparator<EnrollmentRecord> ENROLLMENT_DISPLAY_UID_COMPARATOR = new Comparator<EnrollmentRecord>() {
072: public int compare(EnrollmentRecord o1, EnrollmentRecord o2) {
073: return o1.getUser().getDisplayId().compareToIgnoreCase(
074: o2.getUser().getDisplayId());
075: }
076: };
077:
078: /**
079: * A comparator that sorts enrollments by student display UID (for installations
080: * where a student UID is a number)
081: */
082: static final Comparator<EnrollmentRecord> ENROLLMENT_DISPLAY_UID_NUMERIC_COMPARATOR = new Comparator<EnrollmentRecord>() {
083: public int compare(EnrollmentRecord o1, EnrollmentRecord o2) {
084: long user1DisplayId = Long.parseLong(o1.getUser()
085: .getDisplayId());
086: long user2DisplayId = Long.parseLong(o2.getUser()
087: .getDisplayId());
088: return (int) (user1DisplayId - user2DisplayId);
089: }
090: };
091:
092: private static final int ALL_SECTIONS_SELECT_VALUE = -1;
093:
094: private static Map columnSortMap;
095: private String searchString;
096: private int firstScoreRow;
097: private int maxDisplayedScoreRows;
098: private int scoreDataRows;
099: private boolean emptyEnrollments; // Needed to render buttons
100: private String defaultSearchString;
101:
102: // The section selection menu will include some choices that aren't
103: // real sections (e.g., "All Sections" or "Unassigned Students".
104: private Integer selectedSectionFilterValue = new Integer(
105: ALL_SECTIONS_SELECT_VALUE);
106: private List sectionFilterSelectItems;
107: private List availableSections; // The real sections accessible by this user
108:
109: // We only store grader UIDs in the grading event history, but the
110: // log displays grader names instead. This map cuts down on possibly expensive
111: // calls to the user directory service.
112: private Map graderIdToNameMap;
113:
114: public EnrollmentTableBean() {
115: maxDisplayedScoreRows = getPreferencesBean()
116: .getDefaultMaxDisplayedScoreRows();
117: }
118:
119: static {
120: columnSortMap = new HashMap();
121: columnSortMap.put(PreferencesBean.SORT_BY_NAME,
122: ENROLLMENT_NAME_COMPARATOR);
123: columnSortMap.put(PreferencesBean.SORT_BY_UID,
124: ENROLLMENT_DISPLAY_UID_COMPARATOR);
125: }
126:
127: // Searching
128: public String getSearchString() {
129: return searchString;
130: }
131:
132: public void setSearchString(String searchString) {
133: if (StringUtils.trimToNull(searchString) == null) {
134: searchString = defaultSearchString;
135: }
136: if (!StringUtils.equals(searchString, this .searchString)) {
137: if (log.isDebugEnabled())
138: log.debug("setSearchString " + searchString);
139: this .searchString = searchString;
140: setFirstRow(0); // clear the paging when we update the search
141: }
142: }
143:
144: public void search(ActionEvent event) {
145: // We don't need to do anything special here, since init will handle the search
146: if (log.isDebugEnabled())
147: log.debug("search");
148: }
149:
150: public void clear(ActionEvent event) {
151: if (log.isDebugEnabled())
152: log.debug("clear");
153: setSearchString(null);
154: }
155:
156: // Sorting
157: public void sort(ActionEvent event) {
158: setFirstRow(0); // clear the paging whenever we update the sorting
159: }
160:
161: public abstract boolean isSortAscending();
162:
163: public abstract void setSortAscending(boolean sortAscending);
164:
165: public abstract String getSortColumn();
166:
167: public abstract void setSortColumn(String sortColumn);
168:
169: // Paging.
170:
171: /**
172: * This method more or less turns a JSF input component (namely the Sakai Pager tag)
173: * into a JSF command component. We want the Pager to cause immediate pseudo-navigation
174: * to a new state, throwing away any score input values without bothering to
175: * validate them. But because the Pager is a UIInput component, it doesn't
176: * have an action method that will be called before full validation is done.
177: * Instead, we declare our Pager input tag to be immediate, set this
178: * valueChangeListener, and explicitly jump over all other intervening
179: * phases directly to the rendering phase, which should then pick up the new paging
180: * values.
181: */
182: public void changePagingState(ValueChangeEvent valueChange) {
183: if (log.isDebugEnabled())
184: log.debug("changePagingState: old="
185: + valueChange.getOldValue() + ", new="
186: + valueChange.getNewValue());
187: FacesContext.getCurrentInstance().renderResponse();
188: }
189:
190: public int getFirstRow() {
191: if (log.isDebugEnabled())
192: log.debug("getFirstRow " + firstScoreRow);
193: return firstScoreRow;
194: }
195:
196: public void setFirstRow(int firstRow) {
197: if (log.isDebugEnabled())
198: log.debug("setFirstRow from " + firstScoreRow + " to "
199: + firstRow);
200: firstScoreRow = firstRow;
201: }
202:
203: public int getMaxDisplayedRows() {
204: return maxDisplayedScoreRows;
205: }
206:
207: public void setMaxDisplayedRows(int maxDisplayedRows) {
208: maxDisplayedScoreRows = maxDisplayedRows;
209: }
210:
211: public int getDataRows() {
212: return scoreDataRows;
213: }
214:
215: private boolean isFilteredSearch() {
216: return !StringUtils.equals(searchString, defaultSearchString);
217: }
218:
219: protected Map getOrderedEnrollmentMap() {
220: List enrollments = getWorkingEnrollments();
221:
222: scoreDataRows = enrollments.size();
223: emptyEnrollments = enrollments.isEmpty();
224:
225: return transformToOrderedEnrollmentMap(enrollments);
226: }
227:
228: protected List getWorkingEnrollments() {
229: List enrollments;
230:
231: if (isFilteredSearch()) {
232: String sectionUid;
233: if (isAllSectionsSelected()) {
234: sectionUid = null;
235: } else {
236: sectionUid = getSelectedSectionUid();
237: }
238: enrollments = findMatchingEnrollments(searchString,
239: sectionUid);
240: } else if (isAllSectionsSelected()) {
241: enrollments = getAvailableEnrollments();
242: } else {
243: // The user has selected a particular section.
244: enrollments = getSectionEnrollments(getSelectedSectionUid());
245: }
246:
247: return enrollments;
248: }
249:
250: private Map transformToOrderedEnrollmentMap(List enrollments) {
251: Map enrollmentMap;
252: if (isEnrollmentSort()) {
253: Collections.sort(enrollments, (Comparator) columnSortMap
254: .get(getSortColumn()));
255: enrollments = finalizeSortingAndPaging(enrollments);
256: enrollmentMap = new LinkedHashMap(); // Preserve ordering
257: } else {
258: enrollmentMap = new HashMap();
259: }
260:
261: for (Iterator iter = enrollments.iterator(); iter.hasNext();) {
262: EnrollmentRecord enr = (EnrollmentRecord) iter.next();
263: enrollmentMap.put(enr.getUser().getUserUid(), enr);
264: }
265:
266: return enrollmentMap;
267: }
268:
269: protected List finalizeSortingAndPaging(List list) {
270: List finalList;
271: if (!isSortAscending()) {
272: Collections.reverse(list);
273: }
274: if (maxDisplayedScoreRows == 0) {
275: finalList = list;
276: } else {
277: int nextPageRow = Math.min(firstScoreRow
278: + maxDisplayedScoreRows, scoreDataRows);
279: finalList = new ArrayList(list.subList(firstScoreRow,
280: nextPageRow));
281: if (log.isDebugEnabled())
282: log.debug("finalizeSortingAndPaging subList "
283: + firstScoreRow + ", " + nextPageRow);
284: }
285: return finalList;
286: }
287:
288: public boolean isEnrollmentSort() {
289: String sortColumn = getSortColumn();
290: return (sortColumn.equals(PreferencesBean.SORT_BY_NAME) || sortColumn
291: .equals(PreferencesBean.SORT_BY_UID));
292: }
293:
294: protected void init() {
295: graderIdToNameMap = new HashMap();
296:
297: defaultSearchString = getLocalizedString("search_default_student_search_string");
298: if (searchString == null) {
299: searchString = defaultSearchString;
300: }
301:
302: // Section filtering.
303: availableSections = getAvailableSections();
304: sectionFilterSelectItems = new ArrayList();
305:
306: // The first choice is always "All available enrollments"
307: sectionFilterSelectItems.add(new SelectItem(new Integer(
308: ALL_SECTIONS_SELECT_VALUE), FacesUtil
309: .getLocalizedString("search_sections_all")));
310:
311: // TODO If there are unassigned students and the current user is allowed to see them, add them next.
312:
313: // Add the available sections.
314: for (int i = 0; i < availableSections.size(); i++) {
315: CourseSection section = (CourseSection) availableSections
316: .get(i);
317: sectionFilterSelectItems.add(new SelectItem(new Integer(i),
318: section.getTitle()));
319: }
320:
321: // If the selected value now falls out of legal range due to sections
322: // being deleted, throw it back to the default value (meaning everyone).
323: int selectedSectionVal = selectedSectionFilterValue.intValue();
324: if ((selectedSectionVal >= 0)
325: && (selectedSectionVal >= availableSections.size())) {
326: if (log.isInfoEnabled())
327: log.info("selectedSectionFilterValue="
328: + selectedSectionFilterValue.intValue()
329: + " but available sections="
330: + availableSections.size());
331: selectedSectionFilterValue = new Integer(
332: ALL_SECTIONS_SELECT_VALUE);
333: }
334: }
335:
336: public boolean isAllSectionsSelected() {
337: return (selectedSectionFilterValue.intValue() == ALL_SECTIONS_SELECT_VALUE);
338: }
339:
340: public String getSelectedSectionUid() {
341: int filterValue = selectedSectionFilterValue.intValue();
342: if (filterValue == ALL_SECTIONS_SELECT_VALUE) {
343: return null;
344: } else {
345: CourseSection section = (CourseSection) availableSections
346: .get(filterValue);
347: return section.getUuid();
348: }
349: }
350:
351: public Integer getSelectedSectionFilterValue() {
352: return selectedSectionFilterValue;
353: }
354:
355: public void setSelectedSectionFilterValue(
356: Integer selectedSectionFilterValue) {
357: if (!selectedSectionFilterValue
358: .equals(this .selectedSectionFilterValue)) {
359: this .selectedSectionFilterValue = selectedSectionFilterValue;
360: setFirstRow(0); // clear the paging when we update the search
361: }
362: }
363:
364: public List getSectionFilterSelectItems() {
365: return sectionFilterSelectItems;
366: }
367:
368: public boolean isEmptyEnrollments() {
369: return emptyEnrollments;
370: }
371:
372: // Map grader UIDs to grader names for the grading event log.
373: public String getGraderNameForId(String graderId) {
374: String graderName = (String) graderIdToNameMap.get(graderId);
375: if (graderName == null) {
376: try {
377: graderName = getUserDirectoryService()
378: .getUserDisplayName(graderId);
379: } catch (UnknownUserException e) {
380: log.warn("Unable to find grader with uid=" + graderId);
381: graderName = graderId;
382: }
383: graderIdToNameMap.put(graderId, graderName);
384: }
385: return graderName;
386: }
387:
388: // Support grading event logs.
389: public class GradingEventRow implements Serializable {
390: private Date date;
391: private String graderName;
392: private String grade;
393:
394: public GradingEventRow(GradingEvent gradingEvent) {
395: date = gradingEvent.getDateGraded();
396: grade = gradingEvent.getGrade();
397: graderName = getGraderNameForId(gradingEvent.getGraderId());
398: }
399:
400: public Date getDate() {
401: return date;
402: }
403:
404: public String getGrade() {
405: return grade;
406: }
407:
408: public String getGraderName() {
409: return graderName;
410: }
411: }
412: }
|