Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/batching.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/batching.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/batching.py	(revision 9169)
@@ -304,4 +304,5 @@
         finished_writer = csv.DictWriter(open(finished_path, 'wb'),
                                          finished_headers)
+        os.chmod(finished_path, 0664)
         finished_writer.writerow(dict([(x,x) for x in finished_headers]))
 
@@ -398,4 +399,15 @@
                 os.path.abspath(finished_path), failed_path)
 
+    def get_csv_skeleton(self):
+        """Export CSV file only with a header of available fields.
+
+        A raw string with CSV data should be returned.
+        """
+        outfile = StringIO()
+        writer = csv.DictWriter(outfile, self.available_fields)
+        writer.writerow(dict(zip(self.available_fields, self.available_fields))) # header
+        outfile.seek(0)
+        return outfile.read()
+
 class ExporterBase(object):
     """A base for exporters.
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/browser.py	(revision 9169)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/browser.py	(revision 9169)
@@ -0,0 +1,200 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program 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 General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""UI components for utilities and helpers.
+"""
+
+import grok
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from waeup.kofa.browser.layout import UtilityView
+from waeup.kofa.interfaces import IObjectHistory
+
+from waeup.kofa.interfaces import IUniversity
+
+def replaceStudentMessages(old, new):
+    students = grok.getSite()['students']
+    for student in students.values():
+        history = IObjectHistory(student)
+        history.modifyMessages(old, new)
+    return
+
+def replaceApplicantMessages(old, new):
+    applicants = grok.getSite()['applicants']
+    for container in applicants.values():
+        for applicant in container.values():
+            history = IObjectHistory(applicant)
+            history.modifyMessages(old, new)
+    return
+
+def removeStudentMessage(student_id, number):
+    students = grok.getSite()['students']
+    student = students.get(student_id, None)
+    if student:
+        history = IObjectHistory(student)
+        success, text = history.removeMessage(number)
+    return success, text
+
+def removeApplicantMessage(applicant_id, number):
+    applicants = grok.getSite()['applicants']
+    try:
+        container, application_number = applicant_id.split('_')
+    except:
+        return False, 'applicant_id is wrong'
+    container = applicants.get(container, None)
+    if not container:
+        return False, 'No such container'
+    applicant = container.get(application_number, None)
+    if applicant is None:
+        return False, 'No such applicant'
+    history = IObjectHistory(applicant)
+    success, text = history.removeMessage(number)
+    return success, text
+
+class ReindexPage(UtilityView, grok.View):
+    """ Reindex view.
+
+    Reindexes a catalog. For managers only.
+    """
+    grok.context(IUniversity)
+    grok.name('reindex')
+    grok.require('waeup.managePortal')
+
+    def update(self,ctlg=None):
+        if ctlg is None:
+            self.flash('No catalog name provided.')
+            return
+        cat = queryUtility(ICatalog, name='%s_catalog' % ctlg)
+        if cat is None:
+            self.flash('%s_catalog does not exist' % ctlg)
+            return
+        self.context.logger.info(
+            'Catalog `%s_catalog` re-indexing started.' % ctlg)
+        cat.updateIndexes()
+        no_of_entries = cat.values()[0].documentCount()
+        self.flash('%d %s re-indexed.' % (no_of_entries,ctlg))
+        self.context.logger.info(
+            'Re-indexing of %d objects finished.' % no_of_entries)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class ModifyAllStudentHistory(UtilityView, grok.View):
+    """ View to modify all student histories.
+
+    """
+    grok.context(IUniversity)
+    grok.name('modify_student_history')
+    grok.require('waeup.managePortal')
+
+    def update(self,old=None, new=None):
+        if None in (old, new):
+            self.flash('Syntax: /modify_student_history?old=[old string]&new=[new string]')
+            return
+        replaceStudentMessages(old, new)
+        self.context.logger.info(
+            "'%s' replaced by '%s' in all student histories." % (old, new))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class RemoveStudentHistoryMessage(UtilityView, grok.View):
+    """ View to remove a single student history entry.
+
+    """
+    grok.context(IUniversity)
+    grok.name('remove_student_history_message')
+    grok.require('waeup.managePortal')
+
+    def update(self,student_id=None, number=None):
+        if None in (student_id, number):
+            self.flash('Syntax: /remove_student_history_message?student_id=[id]&number=[line number, starting with 0]')
+            return
+        try:
+            number=int(number)
+        except:
+            self.flash('Error')
+            return
+        success, text = removeStudentMessage(student_id, number)
+        if not success:
+            self.flash('Error: %s' % text)
+            return
+        self.context.logger.info(
+            "line '%s' removed in %s history" % (text, student_id))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class ModifyAllApplicantHistory(UtilityView, grok.View):
+    """ View to modify all student histories.
+
+    """
+    grok.context(IUniversity)
+    grok.name('modify_applicant_history')
+    grok.require('waeup.managePortal')
+
+    def update(self,old=None, new=None):
+        if None in (old, new):
+            self.flash('Syntax: /modify_applicant_history?old=[old string]&new=[new string]')
+            return
+        replaceApplicantMessages(old, new)
+        self.context.logger.info(
+            "'%s' replaced by '%s' in all applicant histories." % (old, new))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class RemoveApplicantHistoryMessage(UtilityView, grok.View):
+    """ View to remove a single applicant history entry.
+
+    """
+    grok.context(IUniversity)
+    grok.name('remove_applicant_history_message')
+    grok.require('waeup.managePortal')
+
+    def update(self,applicant_id=None, number=None):
+        if None in (applicant_id, number):
+            self.flash('Syntax: /remove_applicant_history_message?applicant_id=[id]&number=[line number, starting with 0]')
+            return
+        try:
+            number=int(number)
+        except:
+            self.flash('Error')
+            return
+        success, text = removeApplicantMessage(applicant_id, number)
+        if not success:
+            self.flash('Error: %s' % text)
+            return
+        self.context.logger.info(
+            "line '%s' removed in %s history" % (text, applicant_id))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/helpers.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/helpers.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/helpers.py	(revision 9169)
@@ -520,4 +520,6 @@
             if exclude_methods and isinstance(descr, Method):
                 continue
+            if name in result:
+                continue
             result.append(name)
     return result
@@ -683,15 +685,2 @@
         writer.writerow(row)
     return tmp_path
-
-# Save function used for save methods in pages
-def msave(view, **data):
-    changed_fields = view.applyData(view.context, **data)
-    # Turn list of lists into single list
-    if changed_fields:
-        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
-    fields_string = ' + '.join(changed_fields)
-    view.flash(_('Form has been saved.'))
-    ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
-    if fields_string:
-        grok.getSite().logger.info('%s - saved: %s' % (ob_class, fields_string))
-    return
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/tests/test_browser.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/tests/test_browser.py	(revision 9169)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/tests/test_browser.py	(revision 9169)
@@ -0,0 +1,139 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program 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 General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the student-related UI components.
+"""
+import shutil
+import tempfile
+import pytz
+from datetime import datetime, timedelta
+from StringIO import StringIO
+import os
+import grok
+from zope.event import notify
+from zope.component import createObject, queryUtility
+from zope.component.hooks import setSite, clearSite
+from zope.catalog.interfaces import ICatalog
+from zope.security.interfaces import Unauthorized
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+from waeup.kofa.utils.browser import replaceStudentMessages
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+
+class StudentUtilsUITests(StudentsFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_replace_student_messages(self):
+        self.assertTrue('Record created by system' in
+            self.student.history.messages[0])
+        replaceStudentMessages('system', 'me')
+        self.assertTrue('Record created by me' in
+            self.student.history.messages[0])
+
+    def test_modify_all_student_history(self):
+        self.assertTrue('Record created by system' in
+            self.student.history.messages[0])
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/modify_student_history')
+        self.assertTrue(
+            'Syntax: /modify_student_history?old=[old string]&new=[new string]'
+            in self.browser.contents)
+        self.browser.open(
+            'http://localhost/app/modify_student_history?old=by system&new=by me')
+        self.assertTrue('Finished' in self.browser.contents)
+        self.assertTrue('Record created by me' in
+            self.student.history.messages[0])
+
+    def test_remove_student_history_message(self):
+        self.assertTrue('Record created by system' in
+            self.student.history.messages[0])
+        self.assertEqual(len(self.student.history.messages),1)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/remove_student_history_message')
+        self.assertTrue(
+            'Syntax: /remove_student_history_message?student_id=[id]&number=[line number, starting with 0]'
+            in self.browser.contents)
+        self.browser.open(
+            'http://localhost/app/remove_student_history_message?student_id=%s&number=0' % self.student.student_id)
+        self.assertTrue('Finished' in self.browser.contents)
+        self.assertEqual(len(self.student.history.messages),0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertMatches(
+            "...zope.mgr - line '<YYYY-MM-DD hh:mm:ss> UTC - "
+            "Record created by system' removed in K1000000 history",
+            logcontent)
+
+    def test_reindex(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/reindex')
+        self.assertTrue('No catalog name provided' in self.browser.contents)
+        self.browser.open('http://localhost/app/reindex?ctlg=xyz')
+        self.assertTrue('xyz_catalog does not exist' in self.browser.contents)
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(student_id=(None, None))
+        self.assertEqual(len(results),1)
+        cat.clear()
+        results = cat.searchResults(student_id=(None, None))
+        self.assertEqual(len(results),0)
+        self.browser.open('http://localhost/app/reindex?ctlg=students')
+        self.assertTrue('1 students re-indexed' in self.browser.contents)
+        results = cat.searchResults(student_id=(None, None))
+        self.assertEqual(len(results),1)
+
+class ApplicantUtilsUITests(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_modify_all_applicant_history(self):
+        self.assertTrue('Application initialized by system' in
+            self.applicant.history.messages[0])
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/modify_applicant_history')
+        self.assertTrue(
+            'Syntax: /modify_applicant_history?old=[old string]&new=[new string]'
+            in self.browser.contents)
+        self.browser.open(
+            'http://localhost/app/modify_applicant_history?old=by system&new=by me')
+        self.assertTrue('Finished' in self.browser.contents)
+        self.assertTrue('Application initialized by me' in
+            self.applicant.history.messages[0])
+
+    def test_remove_applicant_history_message(self):
+        self.assertTrue('Application initialized by system' in
+            self.applicant.history.messages[0])
+        self.assertEqual(len(self.applicant.history.messages),1)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/remove_applicant_history_message')
+        self.assertTrue(
+            'Syntax: /remove_applicant_history_message?applicant_id=[id]&number=[line number, starting with 0]'
+            in self.browser.contents)
+        self.browser.open(
+            'http://localhost/app/remove_applicant_history_message?applicant_id=%s&number=0' % self.applicant.applicant_id)
+        self.assertTrue('Finished' in self.browser.contents)
+        self.assertEqual(len(self.applicant.history.messages),0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertMatches(
+            "...zope.mgr - line '<YYYY-MM-DD hh:mm:ss> UTC - "
+            "Application initialized by system' removed in %s history"
+            % self.applicant.applicant_id, logcontent)
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/utils.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/utils.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/utils/utils.py	(revision 9169)
@@ -123,4 +123,5 @@
 
     STUDY_MODES_DICT = {
+        'transfer': 'Transfer',
         'ug_ft': 'Undergraduate Full-Time',
         'ug_pt': 'Undergraduate Part-Time',
@@ -192,5 +193,5 @@
 
 
-    def sendCredentials(self, user, password=None, login_url=None, msg=None):
+    def sendCredentials(self, user, password=None, url_info=None, msg=None):
         """Send credentials as email.
 
@@ -209,5 +210,5 @@
 Your user name: ${d}
 Your password: ${e}
-Login page: ${f}
+${f}
 
 Please remember your user name and keep
@@ -230,5 +231,5 @@
             'd':user.name,
             'e':password,
-            'f':login_url})
+            'f':url_info})
 
         body = translate(text, 'waeup.kofa',
