Index: server/lib/gutenbach/server/job.py
===================================================================
--- server/lib/gutenbach/server/job.py	(revision a2b05827df68b47ce9fa7397689782f571172a24)
+++ server/lib/gutenbach/server/job.py	(revision 951ab1bdfe4dd0ee569e0d9fdad9d257eac7623b)
@@ -1,3 +1,3 @@
-from .errors import InvalidJobStateException, MissingDataException
+from . import errors
 from .player import Player
 from gutenbach.ipp import JobStates as States
@@ -23,8 +23,9 @@
         self.name = name
         self.priority = priority
-        self._why_done = None
 
         if document is not None:
             self.spool(document)
+
+        self._why_done = None
 
     def __repr__(self):
@@ -52,5 +53,5 @@
         try:
             self._id = max(int(val), -1)
-        except TypeError:
+        except:
             self._id = -1
 
@@ -62,5 +63,5 @@
         try:
             self._priority = max(int(val), 1)
-        except TypeError:
+        except:
             self._priority = 1
 
@@ -103,4 +104,8 @@
         return size
 
+    ######################################################################
+    ###                            State                               ###
+    ######################################################################
+
     @property
     def is_valid(self):
@@ -145,4 +150,8 @@
     @property
     def is_done(self):
+        """Whether the job is done playing, regardless of whether it
+        completed successfully or not.
+
+        """
         return (self.is_valid and \
                 self.player is not None and \
@@ -150,4 +159,25 @@
                 (self._why_done == "cancelled" or \
                  self._why_done == "aborted")
+
+    @property
+    def is_completed(self):
+        """Whether the job completed successfully.
+
+        """
+        return self.is_done and self._why_done == "completed"
+
+    @property
+    def is_cancelled(self):
+        """Whether the job was cancelled.
+
+        """
+        return self.is_done and self._why_done == "cancelled"
+
+    @property
+    def is_aborted(self):
+        """Whether the job was aborted.
+
+        """
+        return self.is_done and self._why_done == "aborted"
 
     @property
@@ -167,9 +197,9 @@
         elif self.is_playing and self.is_paused:
             state = States.STOPPED
-        elif self.is_done and self._why_done == "completed":
+        elif self.is_completed:
             state = States.COMPLETE
-        elif self.is_done and self._why_done == "cancelled":
+        elif self.is_cancelled:
             state = States.CANCELLED
-        elif self.is_done and self._why_done == "aborted":
+        elif self.is_aborted:
             state = States.ABORTED
         else:
@@ -181,7 +211,30 @@
     ######################################################################
 
-    def spool(self, document):
-        if not self.is_valid:
-            raise InvalidJobStateException(self.state)
+    @staticmethod
+    def verify_document(document):
+        if not hasattr(document, "name"):
+            raise errors.InvalidDocument, "no name attribute"
+        if not hasattr(document, "read"):
+            raise errors.InvalidDocument, "no read attribute"
+        if not hasattr(document, "close"):
+            raise errors.InvalidDocument, "no close attribute"
+
+    def spool(self, document=None):
+        """Non-blocking spool.  Job must be valid, and the document
+        must be an open file handler.
+
+        Raises
+        ------
+        InvalidDocument
+            If the document is not valid.
+        InvalidJobStateException
+            If the job is not valid or it is already
+            spooled/ready/finished.
+
+        """
+
+        if not self.is_valid or self.state != States.HELD:
+            raise errors.InvalidJobStateException(self.state)
+        self.verify_document(document)
         self.document = document.name
         self.player = Player(document)
@@ -189,6 +242,5 @@
 
     def play(self):
-        """Non-blocking play function.  Sets the job state to
-        PROCESSING.
+        """Non-blocking play.  Job must be ready.
 
         Raises
@@ -202,5 +254,5 @@
         # valid
         if not self.is_ready:
-            raise InvalidJobStateException(self.state)
+            raise errors.InvalidJobStateException(self.state)
         
         # and set the state to processing if we're good to go
@@ -214,14 +266,30 @@
 
     def pause(self):
-        """Non-blocking pause function.  Sets the job state to
-        STOPPED.
+        """Non-blocking pause.  Job must be playing.
+
+        Raises
+        ------
+        InvalidJobStateException
+            If the job is not playing.
 
         """
         
         if not self.is_playing:
-            raise InvalidJobStateException(self.state)
+            raise errors.InvalidJobStateException(self.state)
         self.player.mplayer_pause()
 
     def cancel(self):
+        """Non-blocking cancel. The job must not have previously
+        finished (i.e., cannot be aborted, cancelled, or completed).
+        This should be used to stop the job following an external
+        request.
+
+        Raises
+        ------
+        InvalidJobStateException
+            If the job has already finished.
+
+        """
+        
         def _cancelled():
             logger.info("cancelled job %s" % str(self))
@@ -232,9 +300,20 @@
             self.player.mplayer_stop()
         elif self.is_done and not self._why_done == "cancelled":
-            raise InvalidJobStateException(self.state)
+            raise errors.InvalidJobStateException(self.state)
         else:
             _cancelled()
 
     def abort(self):
+        """Non-blocking abort. The job must not have previously
+        finished (i.e., cannot be aborted, cancelled, or completed).
+        This should be used to stop the job following internal errors.
+
+        Raises
+        ------
+        InvalidJobStateException
+            If the job has already finished.
+
+        """
+
         def _aborted():
             logger.info("aborted job %s" % str(self))
@@ -245,5 +324,5 @@
             self.player.mplayer_stop()
         elif self.is_done and not self._why_done == "aborted":
-            raise InvalidJobStateException(self.state)
+            raise errors.InvalidJobStateException(self.state)
         else:
             _aborted()
