Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py	(revision 5131)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py	(revision 5132)
@@ -198,4 +198,15 @@
     grok.implements(IAccessCodeBatchContainer)
 
+    def _getStoragePath(self):
+        """Get the directory, where batch import files are stored.
+        """
+        site = grok.getSite()
+        storagepath = site['datacenter'].storage
+        ac_storage = os.path.join(storagepath, 'accesscodes')
+        import_path = os.path.join(ac_storage, 'imports')
+        if not os.path.exists(import_path):
+            os.mkdir(import_path)
+        return import_path
+
     def addBatch(self, batch):
         """Add a batch.
@@ -224,4 +235,39 @@
         return num
 
+    def getImportFiles(self):
+        """Return a generator with basenames of available import files.
+        """
+        path = self._getStoragePath()
+        for filename in sorted(os.listdir(path)):
+            yield filename
+    
+    def reimport(self, filename, creator=u'UNKNOWN'):
+        """Reimport a batch given in CSV file.
+
+        CSV file must be of format as generated by createCSVLogFile
+        method.
+        """
+        path = os.path.join(self._getStoragePath(), filename)
+        reader = csv.DictReader(open(path, 'rb'), quoting=csv.QUOTE_ALL)
+        entry = reader.next()
+        cost = float(entry['cost'])
+        num = int(entry['ac'])
+        batch_name = '%s-%s' % (entry['serial'], num)
+        if batch_name in self.keys():
+            raise KeyError('Batch already exists: %s' % batch_name)
+        batch = AccessCodeBatch(
+            datetime.now(), creator, entry['serial'], cost, 0, num=num)
+        num_entries = 0
+        for row in reader:
+            pin = row['ac']
+            serial = int(row['serial'])
+            rand_num = pin.rsplit('-', 1)[-1]
+            batch.addAccessCode(serial, rand_num)
+            num_entries += 1
+        batch.entry_num = num_entries
+        self[batch_name] = batch
+        batch.createCSVLogFile()
+        return
+    
 class AccessCodePlugin(grok.GlobalUtility):
     grok.name('accesscodes')
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt	(revision 5131)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt	(revision 5132)
@@ -330,4 +330,17 @@
       the same prefix, the new one will get the lowest unused number.
 
+   .. method:: getImportFiles()
+
+      Get a list of basenames of available import files, suitable for
+      feeding :meth:`reimport`.
+
+   .. method:: reimport(filename, creator=u'UNKNOWN')
+
+      Reimport a CSV log file of previously created AC batch.
+
+      ``filename`` is the name (basename) of the file residing in the
+      accesscode storage's ``import`` directory.``creator`` is the
+      user ID of the current user.
+
 
 Access code plugin
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser.py	(revision 5131)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser.py	(revision 5132)
@@ -72,4 +72,6 @@
 
 class ReimportBatchPage(WAeUPPage):
+    """Screen for reimporting AC batches.
+    """
     grok.name('reimport')
     grok.context(IAccessCodeBatchContainer)
@@ -80,5 +82,28 @@
     pnav = 0
 
+    def update(self, filenames=None, reimport=None, cancel=None):
+        if cancel is not None:
+            self.flash('Reimport cancelled.')
+            self.redirect(self.url(self.context))
+            return
+        if reimport is None:
+            return
+        if not filenames:
+            self.flash('No file chosen. Action cancelled.')
+            self.redirect(self.url(self.context))
+            return
+        if isinstance(filenames, basestring):
+            filenames = [filenames]
+        userid = self.request.principal.id
+        for filename in filenames:
+            try:
+                self.context.reimport(filename, userid)
+            except KeyError:
+                self.flash('This batch already exists: %s' % filename)
+                continue
+            self.flash('Successfully reimported: %s' % filename)
+        self.redirect(self.url(self.context))
         
+    
 class BatchContainerBreadcrumb(Breadcrumb):
     """A breadcrumb for ac batch containers.
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser.txt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser.txt	(revision 5131)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser.txt	(revision 5132)
@@ -183,4 +183,79 @@
     ...Archived BLA-1 (BLA-1_archive-...-zope.mgr.csv)...
     ...Deleted batch BLA-1...
+    ...
+
+
+Reimporting Batches
+===================
+
+We can reimport batches using the log files written when a batch was
+created before. So one can reimport the freshly deleted BLA-1
+batch.
+
+To do so we must copy the logfile into the ``imports`` dir of
+accesscodes inside the university's datacenter storage. Otherwisae the
+list of importable files is empty:
+
+    >>> browser.getLink('Reimport Scratch Card Batch').click()
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    ...No import batches available...
+    ...
+
+We can cancel that operation:
+
+    >>> browser.getControl('Cancel').click()
+
+We copy the ``BLA-1`` batch logfile over to the ``imports`` directory:
+
+    >>> ac_storage = os.path.join(uploadpath, 'accesscodes')
+    >>> logfile2 = os.path.join(ac_storage, 
+    ...                         sorted(os.listdir(ac_storage))[-3])
+    >>> filename = os.path.basename(logfile2)
+    >>> filename
+    'BLA-1-...-zope.mgr.csv'
+
+    >>> import shutil
+    >>> import_path = os.path.join(ac_storage, 'imports')
+    >>> shutil.copy(logfile2, import_path)
+
+Now the file will be presented as import source:
+
+    >>> browser.getLink('Reimport Scratch Card Batch').click()
+    >>> filename in browser.contents
+    True
+
+If we do not tick a filename to import and click 'Reimport', we will
+be warned:
+
+    >>> browser.getControl('Reimport').click()
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...No file chosen. Action cancelled...
+
+Now let's really reimport the batch:
+
+    >>> browser.getLink('Reimport Scratch Card Batch').click()
+    >>> ctrl = browser.getControl(name='filenames')
+    >>> ctrl.getControl(value=filename).selected = True
+    >>> browser.getControl('Reimport').click()
+
+The batch does exist now again:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully reimported: BLA-1-...-zope.mgr.csv...
+    ...
+
+If we try to reimport an existing batch, that won't work:
+
+    >>> browser.getLink('Reimport Scratch Card Batch').click()
+    >>> ctrl = browser.getControl(name='filenames')
+    >>> ctrl.getControl(value=filename).selected = True
+    >>> browser.getControl('Reimport').click()
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...This batch already exists: BLA-1-...-zope.mgr.csv...
     ...
 
@@ -219,4 +294,5 @@
 
 
+
 Archive files
 -------------
@@ -227,10 +303,11 @@
 
     >>> sorted(os.listdir(ac_storage))
-    [..., 'BLA-1_archive-...-zope.mgr.csv']
+    [..., 'BLA-1_archive-...-zope.mgr.csv', 'imports']
 
 Archive files eventually also contain infos about invalidation dates
 and have a slightly different format therefore.
 
-    >>> archive_file = os.path.join(ac_storage, os.listdir(ac_storage)[-1])
+    >>> archive_file = os.path.join(ac_storage, 
+    ...                             sorted(os.listdir(ac_storage))[-2])
     >>> print open(archive_file, 'rb').read()
     "prefix","serial","ac","student","date"
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt	(revision 5131)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt	(revision 5132)
@@ -1,1 +1,31 @@
-Hello
+<h3>Reimport Access-Code Batches</h3>
+<p>
+  Please select the files to reimport. Please note, that batches with
+  same name have to be deleted before.
+</p>
+<form method="POST">
+  <table>
+    <thead>
+      <tr>
+        <th>&nbsp;</th>
+	<th>File</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="filename context/getImportFiles"
+	  tal:attributes="class python: repeat['filename'].odd() and 'even' or 'odd'">
+	<td>
+	  <input type="checkbox" name="filenames" value="filename"
+		 tal:attributes="value filename" />
+	</td>
+	<td tal:content="filename">file.name</td>
+      </tr>
+      <tr tal:condition="python: len(list(context.getImportFiles())) == 0">
+	<td colspan="5"><b>No import batches available</b></td>
+      </tr>
+    </tbody>
+  </table>
+
+  <input type="submit" name="cancel" value="Cancel" />
+  <input type="submit" name="reimport" value="Reimport" />
+</form>
