/*==========================================================================*\ | $Id: CourseOffering.java,v 1.6 2012/03/28 13:48:08 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2012 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT 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 Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.core; import com.webobjects.foundation.*; import com.webobjects.eocontrol.*; import er.extensions.eof.ERXKey; import er.extensions.foundation.ERXArrayUtilities; import java.io.File; import org.apache.log4j.Logger; // ------------------------------------------------------------------------- /** * Represents a single offering of a course (i.e., one section in a given * semester). * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.6 $, $Date: 2012/03/28 13:48:08 $ */ public class CourseOffering extends _CourseOffering { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Creates a new CourseOffering object. */ public CourseOffering() { super(); } // ---------------------------------------------------------- /** * Look up a CourseOffering by CRN. Assumes the editing * context is appropriately locked. * @param ec The editing context to use * @param theCrn The CRN to look up * @return The course offering, or null if no such CRN exists */ public static CourseOffering offeringForCrn( EOEditingContext ec, String theCrn) { CourseOffering offering = null; NSArray<CourseOffering> results = objectsMatchingQualifier( ec, CourseOffering.crn.eq(theCrn)); if (results != null && results.count() > 0) { offering = results.objectAtIndex(0); } return offering; } //~ Constants (for key names) ............................................. // Derived Attributes --- public static final String COURSE_NUMBER_KEY = COURSE_KEY + "." + Course.NUMBER_KEY; public static final ERXKey<Integer> courseNumber = new ERXKey<Integer>(COURSE_NUMBER_KEY); //~ Methods ............................................................... // ---------------------------------------------------------- /** * Returns the list of students in this course offering, sorted by * user name. * * @return the sorted list of enrolled students */ public NSArray<User> studentsSortedByPID() { return EOSortOrdering.sortedArrayUsingKeyOrderArray( students(), User.userName.ascInsensitives()); } // ---------------------------------------------------------- /** * Returns the course's department abbreviation combined with * the course's number (e.g., "CS 1705"). * @return the department abbreviation and the course number */ public String compactName() { if ( cachedCompactName == null ) { String myLabel = label(); if ( myLabel == null || myLabel.equals( "" ) ) { myLabel = crn(); } if ( course() == null ) { // !!! log.error( "course offering with no associated course: " + crn() + ((label() == null) ? "" : ("(" + label() + ")"))); // don't cache! return "null (" + myLabel + ")"; } else { cachedCompactName = course().deptNumber() + " (" + myLabel + ")"; } } return cachedCompactName; } // ---------------------------------------------------------- /** * Returns the course's department abbreviation combined with * the course's number (e.g., "CS 1705"). * @return the department abbreviation and the course number */ public String deptNumberAndName() { if ( cachedDeptNumberAndName == null ) { cachedDeptNumberAndName = compactName() + ": " + course().name(); } return cachedDeptNumberAndName; } // ---------------------------------------------------------- /** * Get a short (no longer than 60 characters) description of this coursse * offering, which currently returns {@link #compactName()}. * @return the description */ public String userPresentableDescription() { return compactName(); } // ---------------------------------------------------------- /** * Returns true if the given user is an instructor of this * course offering. * * @param user The user to check * @return true if the user is an instructor of the offering */ public boolean isInstructor(User user) { NSArray<User> myInstructors = instructors(); return (myInstructors.indexOfObject(user) != NSArray.NotFound); } // ---------------------------------------------------------- /** * Returns true if the given user is a grader (TA) for this * course offering. * * @param user The user to check * @return true if the user is a grader for the offering * * @deprecated Use the {@link #isGrader(User)} method instead. */ @Deprecated public boolean isTA( User user ) { return isGrader(user); } // ---------------------------------------------------------- /** * Returns true if the given user is a grader (TA) for this * course offering. * * @param user The user to check * @return true if the user is a grader for the offering */ public boolean isGrader( User user ) { NSArray<User> tas = graders(); return ( ( tas.indexOfObject( user ) ) != NSArray.NotFound ); } // ---------------------------------------------------------- /** * Returns true if the given user is a member of the staff (an * instructor or grader) for this course offering. * * @param user The user to check * @return true if the user is staff for the offering */ public boolean isStaff(User user) { return isInstructor(user) || isGrader(user); } // ---------------------------------------------------------- /** * {@inheritDoc} */ @Override public boolean accessibleByUser(User user) { return isStaff(user); } // ---------------------------------------------------------- /** * Gets the array of graders (TAs) for this course offering. * * @return the array of users who are designated as graders for this * course offering * * @deprecated Use the {@link #graders()} method instead. */ @Deprecated public NSArray<User> TAs() { return graders(); } // ---------------------------------------------------------- /** * Gets the array of users that includes all course staff (instructors or * graders). * * @return the array of users */ public NSArray<User> staff() { NSMutableArray<User> staff = instructors().mutableClone(); ERXArrayUtilities.addObjectsFromArrayWithoutDuplicates( staff, graders()); return staff; } // ---------------------------------------------------------- /** * Gets the array of users that includes students enrolled in this * course offering together with all course staff (instructors or * graders). * * @return the array of users */ public NSArray<User> studentsAndStaff() { NSMutableArray<User> studentsAndStaff = students().mutableClone(); ERXArrayUtilities.addObjectsFromArrayWithoutDuplicates( studentsAndStaff, instructors()); ERXArrayUtilities.addObjectsFromArrayWithoutDuplicates( studentsAndStaff, graders()); return studentsAndStaff; } // ---------------------------------------------------------- /** * Gets the array of students enrolled in this course offering, not * including any course staff (instructors or graders). * * @return the array of students */ public NSArray<User> studentsWithoutStaff() { NSMutableArray<User> studentsWithoutStaff = students().mutableClone(); studentsWithoutStaff.removeObjectsInArray(instructors()); studentsWithoutStaff.removeObjectsInArray(graders()); return studentsWithoutStaff; } // ---------------------------------------------------------- /* (non-Javadoc) * @see org.webcat.core._CourseOffering#setCourse(org.webcat.core.Course) */ public void setCourse( Course value ) { cachedCompactName = null; cachedDeptNumberAndName = null; super.setCourse( value ); } // ---------------------------------------------------------- /** * Change the value of this object's <code>crn</code> * property. * * @param value The new value for this property */ public void setCrn( String value ) { saveOldDirComponents(); cachedSubdirName = null; cachedCompactName = null; cachedDeptNumberAndName = null; super.setCrn( value.trim() ); } // ---------------------------------------------------------- public Object validateCrn( Object value ) { if ( value == null || value.equals("") ) { throw new ValidationException( "Please provide a unique CRN to identify your course " + "offering." ); } NSArray<CourseOffering> others = objectsMatchingQualifier( editingContext(), crn.is(value.toString())); if (others.count() > 1 || (others.count() == 1 && others.objectAtIndex(0) != this)) { throw new ValidationException( "Another course offering with that CRN already exists." ); } return value; } // ---------------------------------------------------------- /** * Change the value of this object's <code>label</code> * property. * * @param value The new value for this property */ public void setLabel( String value ) { cachedCompactName = null; cachedDeptNumberAndName = null; super.setLabel( value ); } // ---------------------------------------------------------- public String crnSubdirName() { if ( cachedSubdirName == null ) { String name = crn(); cachedSubdirName = AuthenticationDomain.subdirNameOf( name ); log.debug( "trimmed name '" + name + "' to '" + cachedSubdirName + "'" ); } return cachedSubdirName; } // ---------------------------------------------------------- /** * Set the entity pointed to by the <code>semester</code> * relationship (DO NOT USE--instead, use * <code>setSemesterRelationship()</code>. * This method is provided for WebObjects use. * * @param value The new entity to relate to */ public void setSemester( Semester value ) { log.debug("setSemester(" + value + ")"); saveOldDirComponents(); cachedSubdirName = null; super.setSemester(value); } // ---------------------------------------------------------- public void takeValueForKey( Object value, String key ) { log.debug("takeValueForKey(" + value + ", " + key + ")"); if (SEMESTER_KEY.equals(key)) { saveOldDirComponents(); cachedSubdirName = null; } super.takeValueForKey( value, key ); } // ---------------------------------------------------------- /* (non-Javadoc) * @see er.extensions.eof.ERXGenericRecord#didUpdate() */ public void didUpdate() { super.didUpdate(); if ( crnDirNeedingRenaming != null || semesterDirNeedingRenaming != null) { renameSubdirs( semesterDirNeedingRenaming, ( semester() == null ? null : semester().dirName() ), crnDirNeedingRenaming, crnSubdirName() ); crnDirNeedingRenaming = null; semesterDirNeedingRenaming = null; } } // ---------------------------------------------------------- /** * Retrieve the name of the subdirectory where all submissions for this * course offering are stored. This subdirectory is relative to * the base submission directory for some authentication domain, such * as the value returned by * {@link #submissionBaseDirName(AuthenticationDomain)}. * @param dir the string buffer to add the requested subdirectory to * (a / is added to this buffer, followed by the subdirectory name * generated here) * @param course the course whose subdir should be added (may not be null). */ public void addSubdirTo( StringBuffer dir ) { dir.append( '/' ); dir.append( semester().dirName() ); dir.append( '/' ); dir.append( crnSubdirName() ); } // ---------------------------------------------------------- @Override public void mightDelete() { log.debug("mightDelete()"); if (isNewObject()) return; if (hasAssignmentOfferings()) { log.debug("mightDelete(): offering has assignments"); throw new ValidationException("You may not delete a course " + "offering that has assignment offerings."); } StringBuffer buf = new StringBuffer("/"); addSubdirTo(buf); subdirToDelete = buf.toString(); super.mightDelete(); } // ---------------------------------------------------------- @Override public boolean canDelete() { boolean result = (course() == null || editingContext() == null || !hasAssignmentOfferings()); log.debug("canDelete() = " + result); return result; } // ---------------------------------------------------------- @Override public void didDelete( EOEditingContext context ) { log.debug("didDelete()"); super.didDelete( context ); // should check to see if this is a child ec EOObjectStore parent = context.parentObjectStore(); if (parent == null || !(parent instanceof EOEditingContext)) { if (subdirToDelete != null) { NSArray<AuthenticationDomain> domains = AuthenticationDomain.authDomains(); for (AuthenticationDomain domain : domains) { StringBuffer dir = domain.submissionBaseDirBuffer(); dir.append(subdirToDelete); File courseDir = new File(dir.toString()); if (courseDir.exists()) { FileUtilities.deleteDirectory(courseDir); } } } } } //~ Private Methods ....................................................... // ---------------------------------------------------------- private boolean hasAssignmentOfferings() { if (isNewObject()) return false; // This method introduces some minor conceptual coupling with // the Grader subsystem. However, it avoids all binary dependencies // and it is necessary to preserve the integrity of the data // model across the subsystems. // This code is basically the same as that in // _AssignmentOffering.objectsForCourseOffering() EOFetchSpecification spec = EOFetchSpecification .fetchSpecificationNamed( "offeringsForCourseOffering", "AssignmentOffering"); NSMutableDictionary<String, Object> bindings = new NSMutableDictionary<String, Object>(); bindings.setObjectForKey( this, "courseOffering" ); spec = spec.fetchSpecificationWithQualifierBindings( bindings ); NSArray<?> result = editingContext().objectsWithFetchSpecification( spec ); if (log.isDebugEnabled()) { log.debug("hasAssignmentOfferings(): fetch = " + result); } return result.count() > 0; } // ---------------------------------------------------------- private void renameSubdirs( String oldSemesterSubdir, String newSemesterSubdir, String oldCrnSubdir, String newCrnSubdir ) { NSArray<AuthenticationDomain> domains = AuthenticationDomain.authDomains(); String msgs = null; for (AuthenticationDomain domain : domains) { StringBuffer dir = domain.submissionBaseDirBuffer(); dir.append('/'); int baseDirLen = dir.length(); dir.append(oldSemesterSubdir); dir.append('/'); dir.append( oldCrnSubdir ); File oldDir = new File( dir.toString() ); log.debug("Checking for: " + oldDir); if ( oldDir.exists() ) { dir.delete( baseDirLen, dir.length() ); dir.append(newSemesterSubdir); // First, make sure that the new dir exists! File newDir = new File( dir.toString() ); if (!newDir.exists()) { newDir.mkdirs(); } dir.append('/'); dir.append( newCrnSubdir ); newDir = new File( dir.toString() ); // Do the renaming log.debug("Renaming: " + oldDir + " => " + newDir); if (!oldDir.renameTo( newDir )) { msgs = (msgs == null ? "" : (msgs + " ")) + "Failed to rename directory: " + oldDir + " => " + newDir; } } } if (msgs != null) { throw new RuntimeException(msgs); } } // ---------------------------------------------------------- private void saveOldDirComponents() { if (crnDirNeedingRenaming == null || semesterDirNeedingRenaming == null) { if ( crnDirNeedingRenaming == null && crn() != null ) { crnDirNeedingRenaming = crnSubdirName(); } if ( semesterDirNeedingRenaming == null && semester() != null ) { semesterDirNeedingRenaming = semester().dirName(); } } } //~ Instance/static variables ............................................. private String cachedSubdirName = null; private String cachedCompactName = null; private String cachedDeptNumberAndName = null; private String semesterDirNeedingRenaming = null; private String crnDirNeedingRenaming = null; private String subdirToDelete; static Logger log = Logger.getLogger( CourseOffering.class ); }