001: /**********************************************************************************
002: * $URL: https://source.sakaiproject.org/svn/sections/tags/sakai_2-4-1/sections-app/src/java/org/sakaiproject/tool/section/jsf/backingbean/AddSectionsBean.java $
003: * $Id: AddSectionsBean.java 29186 2007-04-19 02:35:38Z ajpoland@iupui.edu $
004: ***********************************************************************************
005: *
006: * Copyright (c) 2005, 2006 The Regents of the University of California and The Regents of the University of Michigan
007: *
008: * Licensed under the Educational Community License, Version 1.0 (the "License");
009: * you may not use this file except in compliance with the License.
010: * You may obtain a copy of the License at
011: *
012: * http://www.opensource.org/licenses/ecl1.php
013: *
014: * Unless required by applicable law or agreed to in writing, software
015: * distributed under the License is distributed on an "AS IS" BASIS,
016: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017: * See the License for the specific language governing permissions and
018: * limitations under the License.
019: *
020: **********************************************************************************/package org.sakaiproject.tool.section.jsf.backingbean;
021:
022: import java.io.Serializable;
023: import java.sql.Time;
024: import java.util.ArrayList;
025: import java.util.Collection;
026: import java.util.Iterator;
027: import java.util.List;
028:
029: import javax.faces.context.FacesContext;
030: import javax.faces.event.ActionEvent;
031: import javax.faces.event.ValueChangeEvent;
032: import javax.faces.model.SelectItem;
033:
034: import org.apache.commons.lang.StringUtils;
035: import org.apache.commons.logging.Log;
036: import org.apache.commons.logging.LogFactory;
037: import org.sakaiproject.section.api.coursemanagement.Course;
038: import org.sakaiproject.section.api.coursemanagement.CourseSection;
039: import org.sakaiproject.tool.section.jsf.JsfUtil;
040:
041: /**
042: * Controls the add sections page.
043: *
044: * @author <a href="mailto:jholtzman@berkeley.edu">Josh Holtzman</a>
045: *
046: */
047: public class AddSectionsBean extends CourseDependentBean implements
048: SectionEditor, Serializable {
049: private static final long serialVersionUID = 1L;
050: private static final Log log = LogFactory
051: .getLog(AddSectionsBean.class);
052:
053: private Integer numToAdd;
054: private String category;
055: private List<SelectItem> categoryItems;
056: private List<SelectItem> numSectionsSelectItems;
057: private List<CourseSection> sections;
058: private String rowStyleClasses;
059: private String elementToFocus;
060: private transient boolean sectionsChanged;
061:
062: /**
063: * @inheritDoc
064: */
065: public void init() {
066: if (log.isDebugEnabled())
067: log.debug("sections = " + sections);
068: if (log.isDebugEnabled())
069: log.debug("sectionsChanged = " + sectionsChanged);
070:
071: numSectionsSelectItems = new ArrayList<SelectItem>(10);
072: for (int i = 0; i < 10;) {
073: Integer currVal = ++i;
074: numSectionsSelectItems.add(new SelectItem(currVal));
075: }
076: if (numToAdd == null)
077: numToAdd = 1;
078:
079: if (sections == null || sectionsChanged) {
080: if (log.isDebugEnabled())
081: log.debug("initializing add sections bean");
082: List categories = getSectionCategories();
083: populateSections();
084: categoryItems = new ArrayList<SelectItem>();
085: for (Iterator iter = categories.iterator(); iter.hasNext();) {
086: String cat = (String) iter.next();
087: categoryItems.add(new SelectItem(cat,
088: getCategoryName(cat)));
089: }
090: }
091: }
092:
093: /**
094: * Responds to a change in the sections selector in the UI.
095: *
096: * @param event
097: */
098: public void processChangeNumSections(ValueChangeEvent event) {
099: if (log.isDebugEnabled())
100: log
101: .debug("processing a ui change in number of sections to add");
102: sectionsChanged = true;
103: }
104:
105: public void processChangeSectionsCategory(ValueChangeEvent event) {
106: if (log.isDebugEnabled())
107: log
108: .debug("processing a ui change in category of sections to add");
109: sectionsChanged = true;
110: }
111:
112: public void processAddMeeting(ActionEvent action) {
113: if (log.isDebugEnabled())
114: log.debug("processing an 'add meeting' action from "
115: + this .getClass().getName());
116: int index = Integer.parseInt(JsfUtil
117: .getStringFromParam("sectionIndex"));
118: sections.get(index).getMeetings().add(new LocalMeetingModel());
119: elementToFocus = action.getComponent().getClientId(
120: FacesContext.getCurrentInstance());
121: }
122:
123: /**
124: * Populates the section collection and row css classes.
125: *
126: */
127: private void populateSections() {
128: if (log.isDebugEnabled())
129: log.debug("populating sections");
130: Course course = getCourse();
131:
132: sections = new ArrayList<CourseSection>();
133: StringBuffer rowClasses = new StringBuffer();
134: if (StringUtils.trimToNull(category) != null) {
135: if (log.isDebugEnabled())
136: log.debug("populating sections");
137: String categoryName = getCategoryName(category);
138: int offset = getSectionManager().getSectionsInCategory(
139: getSiteContext(), category).size();
140: for (int i = 1; i <= numToAdd; i++) {
141: LocalSectionModel section = new LocalSectionModel(
142: course, categoryName + (i + offset), category,
143: null);
144: section.getMeetings().add(new LocalMeetingModel());
145: sections.add(section);
146: if (i > 1) {
147: rowClasses.append("nextSectionRow");
148: }
149: if (i < numToAdd) {
150: rowClasses.append(",");
151: }
152: }
153: rowStyleClasses = rowClasses.toString();
154: }
155: }
156:
157: /**
158: * Checks whether a string is currently being used as a title for another section.
159: *
160: * @param title
161: * @param existingSections
162: * @return
163: */
164: private boolean isDuplicateSectionTitle(String title,
165: Collection existingSections) {
166: for (Iterator iter = existingSections.iterator(); iter
167: .hasNext();) {
168: CourseSection section = (CourseSection) iter.next();
169: if (section.getTitle().equals(title)) {
170: if (log.isDebugEnabled())
171: log.debug("Conflicting section name found: "
172: + title);
173: return true;
174: }
175: }
176: return false;
177: }
178:
179: /**
180: * Adds the sections, or generates validation messages for bad inputs.
181: *
182: * @return
183: */
184: public String addSections() {
185: if (validationFails()) {
186: setNotValidated(true);
187: return "failure";
188: }
189:
190: // Validation passed, so save the new sections
191: String courseUuid = getCourse().getUuid();
192: StringBuffer titles = new StringBuffer();
193: String sepChar = JsfUtil
194: .getLocalizedMessage("section_separator");
195:
196: for (Iterator iter = sections.iterator(); iter.hasNext();) {
197: LocalSectionModel sectionModel = (LocalSectionModel) iter
198: .next();
199: titles.append(sectionModel.getTitle());
200: if (iter.hasNext()) {
201: titles.append(sepChar);
202: titles.append(" ");
203: }
204: }
205:
206: getSectionManager().addSections(courseUuid, sections);
207:
208: String[] params = new String[3];
209: params[0] = titles.toString();
210: if (sections.size() == 1) {
211: params[1] = JsfUtil
212: .getLocalizedMessage("add_section_successful_singular");
213: params[2] = JsfUtil.getLocalizedMessage("section_singular");
214: } else {
215: params[1] = JsfUtil
216: .getLocalizedMessage("add_section_successful_plural");
217: params[2] = JsfUtil.getLocalizedMessage("section_plural");
218: }
219: JsfUtil.addRedirectSafeInfoMessage(JsfUtil.getLocalizedMessage(
220: "add_section_successful", params));
221: return "overview";
222: }
223:
224: /**
225: * Since the validation and conversion rules rely on the *relative*
226: * values of one component to another, we can't use JSF validators and
227: * converters. So we check everything here.
228: *
229: * @return
230: */
231: protected boolean validationFails() {
232: Collection<CourseSection> existingSections = getAllSiteSections();
233:
234: // Keep track of whether a validation failure occurs
235: boolean validationFailure = false;
236:
237: // We also need to keep track of whether an invalid time was entered,
238: // so we can skip the time comparisons
239: boolean invalidTimeEntered = false;
240:
241: int sectionIndex = 0;
242: for (Iterator iter = sections.iterator(); iter.hasNext(); sectionIndex++) {
243: LocalSectionModel sectionModel = (LocalSectionModel) iter
244: .next();
245:
246: // Ensure that this title isn't being used by another section
247: if (isDuplicateSectionTitle(sectionModel.getTitle(),
248: existingSections)) {
249: if (log.isDebugEnabled())
250: log
251: .debug("Failed to update section... duplicate title: "
252: + sectionModel.getTitle());
253: String componentId = "addSectionsForm:sectionTable:"
254: + sectionIndex + ":titleInput";
255: JsfUtil.addErrorMessage(JsfUtil.getLocalizedMessage(
256: "section_add_failure_duplicate_title",
257: new String[] { sectionModel.getTitle() }),
258: componentId);
259: validationFailure = true;
260: }
261:
262: // Add this new section to the list of existing sections, so any other new sections don't conflict with this section's title
263: existingSections.add(sectionModel);
264:
265: // Ensure that the user didn't choose to limit the size of the section without specifying a max size
266: if (Boolean.TRUE.toString().equals(
267: sectionModel.getLimitSize())
268: && sectionModel.getMaxEnrollments() == null) {
269: String componentId = "addSectionsForm:sectionTable:"
270: + sectionIndex + ":maxEnrollmentInput";
271: JsfUtil.addErrorMessage(JsfUtil
272: .getLocalizedMessage("sections_specify_limit"),
273: componentId);
274: validationFailure = true;
275: }
276:
277: int meetingIndex = 0;
278: for (Iterator meetingsIterator = sectionModel.getMeetings()
279: .iterator(); meetingsIterator.hasNext(); meetingIndex++) {
280: LocalMeetingModel meeting = (LocalMeetingModel) meetingsIterator
281: .next();
282: if (!meeting.isStartTimeDefault()
283: && isInvalidTime(meeting.getStartTimeString())) {
284: if (log.isDebugEnabled())
285: log
286: .debug("Failed to add section... meeting start time "
287: + meeting.getStartTimeString()
288: + " is invalid");
289:
290: String componentId = "addSectionsForm:sectionTable:"
291: + sectionIndex
292: + ":meetingsTable:"
293: + meetingIndex + ":startTime";
294:
295: JsfUtil
296: .addErrorMessage(
297: JsfUtil
298: .getLocalizedMessage("javax.faces.convert.DateTimeConverter.CONVERSION"),
299: componentId);
300: validationFailure = true;
301: invalidTimeEntered = true;
302: }
303:
304: if (!meeting.isEndTimeDefault()
305: && isInvalidTime(meeting.getEndTimeString())) {
306: if (log.isDebugEnabled())
307: log
308: .debug("Failed to add section... meeting end time "
309: + meeting.getEndTimeString()
310: + " is invalid");
311: String componentId = "addSectionsForm:sectionTable:"
312: + sectionIndex
313: + ":meetingsTable:"
314: + meetingIndex + ":endTime";
315: JsfUtil
316: .addErrorMessage(
317: JsfUtil
318: .getLocalizedMessage("javax.faces.convert.DateTimeConverter.CONVERSION"),
319: componentId);
320: validationFailure = true;
321: invalidTimeEntered = true;
322: }
323:
324: // No need to check this if we already have invalid times
325: if (!invalidTimeEntered
326: && isEndTimeWithoutStartTime(meeting)) {
327: if (log.isDebugEnabled())
328: log
329: .debug("Failed to update section... start time without end time");
330: String componentId = "addSectionsForm:sectionTable:"
331: + sectionIndex
332: + ":meetingsTable:"
333: + meetingIndex + ":startTime";
334: JsfUtil
335: .addErrorMessage(
336: JsfUtil
337: .getLocalizedMessage("section_update_failure_end_without_start"),
338: componentId);
339: validationFailure = true;
340: }
341:
342: if (isInvalidMaxEnrollments(sectionModel)) {
343: if (log.isDebugEnabled())
344: log
345: .debug("Failed to update section... max enrollments is not valid");
346: String componentId = "addSectionsForm:sectionTable:"
347: + sectionIndex + ":maxEnrollmentInput";
348: JsfUtil
349: .addErrorMessage(
350: JsfUtil
351: .getLocalizedMessage(
352: "javax.faces.validator.LongRangeValidator.MINIMUM",
353: new String[] { "0" }),
354: componentId);
355: validationFailure = true;
356: }
357:
358: // Don't bother checking if the time values are invalid
359: if (!invalidTimeEntered
360: && isEndTimeBeforeStartTime(meeting)) {
361: if (log.isDebugEnabled())
362: log
363: .debug("Failed to update section... end time is before start time");
364: String componentId = "addSectionsForm:sectionTable:"
365: + sectionIndex
366: + ":meetingsTable:"
367: + meetingIndex + ":endTime";
368: JsfUtil
369: .addErrorMessage(
370: JsfUtil
371: .getLocalizedMessage("section_update_failure_end_before_start"),
372: componentId);
373: validationFailure = true;
374: }
375: }
376: }
377: return validationFailure;
378: }
379:
380: /**
381: * As part of the crutch for JSF's inability to do validation on relative
382: * values in different components, this method checks whether an end time has
383: * been entered without a start time.
384: *
385: * @param startTime
386: * @param endTime
387: * @return
388: */
389: protected boolean isEndTimeWithoutStartTime(
390: LocalMeetingModel meeting) {
391: if (meeting.getStartTime() == null
392: && meeting.getEndTime() != null) {
393: if (log.isDebugEnabled())
394: log
395: .debug("You can not set an end time without setting a start time.");
396: return true;
397: }
398: return false;
399: }
400:
401: /**
402: * As part of the crutch for JSF's inability to do validation on relative
403: * values in different components, this method checks whether two times, as
404: * expressed by string start and end times and booleans indicating am/pm,
405: * express times where the end time proceeds a start time.
406: *
407: * @param meeting
408: * @return
409: */
410: public static boolean isEndTimeBeforeStartTime(
411: LocalMeetingModel meeting) {
412: String startTime = null;
413: if (!meeting.isStartTimeDefault()) {
414: startTime = meeting.getStartTimeString();
415: }
416:
417: String endTime = null;
418: if (!meeting.isEndTimeDefault()) {
419: endTime = meeting.getEndTimeString();
420: }
421:
422: boolean startTimeAm = meeting.isStartTimeAm();
423: boolean endTimeAm = meeting.isEndTimeAm();
424:
425: if (StringUtils.trimToNull(startTime) != null
426: & StringUtils.trimToNull(endTime) != null) {
427: Time start = JsfUtil.convertStringToTime(startTime,
428: startTimeAm);
429: Time end = JsfUtil.convertStringToTime(endTime, endTimeAm);
430: if (start.after(end)) {
431: if (log.isDebugEnabled())
432: log
433: .debug("You can not set an end time earlier than the start time.");
434: return true;
435: }
436: }
437:
438: if (StringUtils.trimToNull(startTime) != null
439: & StringUtils.trimToNull(endTime) != null) {
440: Time start = JsfUtil.convertStringToTime(startTime,
441: startTimeAm);
442: Time end = JsfUtil.convertStringToTime(endTime, endTimeAm);
443: if (start.equals(end)) {
444: if (log.isDebugEnabled())
445: log
446: .debug("You can not set an end time that same as start time.");
447: return true;
448: }
449: }
450: return false;
451: }
452:
453: /**
454: * As part of the crutch for JSF's inability to do validation on relative
455: * values in different components, this method checks whether a string can
456: * represent a valid time.
457: *
458: * Returns true if the string fails to represent a time. Java's date formatters
459: * allow for impossible field values (eg hours > 12) so we do manual checks here.
460: * Ugh.
461: *
462: * @param str The string that might represent a time.
463: *
464: * @return
465: */
466: protected boolean isInvalidTime(String str) {
467: if (StringUtils.trimToNull(str) == null) {
468: // Empty strings are ok
469: return false;
470: }
471:
472: if (str.indexOf(':') != -1) {
473: // This is a fully specified time
474: String[] sa = str.split(":");
475: if (sa.length != 2) {
476: if (log.isDebugEnabled())
477: log
478: .debug("This is not a valid time... it has more than 1 ':'.");
479: return true;
480: }
481: return outOfRange(sa[0], 2, 1, 12)
482: || outOfRange(sa[1], 2, 0, 59);
483: } else {
484: return outOfRange(str, 2, 1, 12);
485: }
486: }
487:
488: /**
489: * Returns true if the string is longer than len, less than low, or higher than high.
490: *
491: * @param str The string
492: * @param len The max length of the string
493: * @param low The lowest possible numeric value
494: * @param high The highest possible numeric value
495: * @return
496: */
497: private static boolean outOfRange(String str, int len, int low,
498: int high) {
499: if (str.length() > len) {
500: return true;
501: }
502: try {
503: int i = Integer.parseInt(str);
504: if (i < low || i > high) {
505: return true;
506: }
507: } catch (NumberFormatException nfe) {
508: if (log.isDebugEnabled())
509: log.debug("time must be a number");
510: return true;
511: }
512: return false;
513: }
514:
515: private boolean isInvalidMaxEnrollments(
516: LocalSectionModel sectionModel) {
517: return sectionModel.getMaxEnrollments() != null
518: && sectionModel.getMaxEnrollments().intValue() < 0;
519: }
520:
521: public String getCategory() {
522: return category;
523: }
524:
525: public void setCategory(String category) {
526: this .category = category;
527: }
528:
529: public int getNumToAdd() {
530: return numToAdd;
531: }
532:
533: public void setNumToAdd(int numToAdd) {
534: this .numToAdd = numToAdd;
535: }
536:
537: public List<SelectItem> getCategoryItems() {
538: return categoryItems;
539: }
540:
541: public List<SelectItem> getNumSectionsSelectItems() {
542: return numSectionsSelectItems;
543: }
544:
545: public List<CourseSection> getSections() {
546: return sections;
547: }
548:
549: public String getRowStyleClasses() {
550: return rowStyleClasses;
551: }
552:
553: public String getElementToFocus() {
554: return elementToFocus;
555: }
556:
557: public void setElementToFocus(String scrollDepth) {
558: this.elementToFocus = scrollDepth;
559: }
560: }
|